├── .eslintrc.js
├── .github
├── ISSUE_TEMPLATE
│ ├── ♻️-리팩토링-요청.md
│ ├── ✨-기능-추가-요청.md
│ └── 🐛-버그-리포트.md
├── pull_request_template.md
└── workflows
│ └── build-test.yml
├── .gitignore
├── .husky
├── commit-msg
├── pre-commit
└── pre-push
├── .lintstagedrc
├── .npmrc
├── .nvmrc
├── .prettierrc
├── .vscode
├── extensions.json
└── settings.json
├── README.md
├── apps
├── shelter
│ ├── .eslintrc.cjs
│ ├── .gitignore
│ ├── README.md
│ ├── index.html
│ ├── package.json
│ ├── public
│ │ ├── favicon.ico
│ │ ├── mockServiceWorker.js
│ │ └── vite.svg
│ ├── src
│ │ ├── App.tsx
│ │ ├── apis
│ │ │ ├── auth.ts
│ │ │ ├── recruitment.ts
│ │ │ ├── shelter.ts
│ │ │ └── volunteers.ts
│ │ ├── assets
│ │ │ ├── CkCheck.tsx
│ │ │ ├── CkClose.tsx
│ │ │ └── react.svg
│ │ ├── constants
│ │ │ ├── path.ts
│ │ │ └── recruitment.ts
│ │ ├── main.tsx
│ │ ├── mocks
│ │ │ ├── browser.ts
│ │ │ └── handlers
│ │ │ │ ├── auth.ts
│ │ │ │ ├── image.ts
│ │ │ │ ├── manage.ts
│ │ │ │ ├── recruitment.ts
│ │ │ │ ├── recruitmentDetail.ts
│ │ │ │ ├── shelter.ts
│ │ │ │ └── volunteers.ts
│ │ ├── pages
│ │ │ ├── animals
│ │ │ │ ├── detail
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── index.tsx
│ │ │ │ ├── search
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── update
│ │ │ │ │ └── index.tsx
│ │ │ │ └── write
│ │ │ │ │ └── index.tsx
│ │ │ ├── chattings
│ │ │ │ ├── index.tsx
│ │ │ │ └── room
│ │ │ │ │ └── index.tsx
│ │ │ ├── manage
│ │ │ │ ├── apply
│ │ │ │ │ ├── _components
│ │ │ │ │ │ ├── ApplyInfoItem.tsx
│ │ │ │ │ │ ├── ApprovedCountBox.tsx
│ │ │ │ │ │ └── ManageApplyItem.tsx
│ │ │ │ │ └── index.tsx
│ │ │ │ └── attendance
│ │ │ │ │ └── index.tsx
│ │ │ ├── my
│ │ │ │ ├── _hooks
│ │ │ │ │ └── useFetchShelterProfile.ts
│ │ │ │ ├── index.tsx
│ │ │ │ └── reviews
│ │ │ │ │ ├── VolunteerProfile.tsx
│ │ │ │ │ ├── hooks
│ │ │ │ │ └── useFetchShelterReviews.tsx
│ │ │ │ │ └── index.tsx
│ │ │ ├── notfound
│ │ │ │ └── index.tsx
│ │ │ ├── notifications
│ │ │ │ └── index.tsx
│ │ │ ├── settings
│ │ │ │ ├── account
│ │ │ │ │ └── index.tsx
│ │ │ │ └── password
│ │ │ │ │ └── index.tsx
│ │ │ ├── signin
│ │ │ │ └── index.tsx
│ │ │ ├── signup
│ │ │ │ └── index.tsx
│ │ │ └── volunteers
│ │ │ │ ├── _components
│ │ │ │ ├── PlusIcon.tsx
│ │ │ │ ├── RecruitDateText.tsx
│ │ │ │ ├── RecruitSkeleton.tsx
│ │ │ │ ├── RecruitSkeletonList.tsx
│ │ │ │ ├── VolunteerRecruitItem.tsx
│ │ │ │ └── VolunteerRecruitItemButton.tsx
│ │ │ │ ├── _hooks
│ │ │ │ └── useVolunteerRecruitItem.ts
│ │ │ │ ├── _queryOptions
│ │ │ │ └── recruitment.ts
│ │ │ │ ├── _utils
│ │ │ │ └── recruitment.ts
│ │ │ │ ├── detail
│ │ │ │ ├── _hooks
│ │ │ │ │ └── useGetVolunteerDetail.ts
│ │ │ │ └── index.tsx
│ │ │ │ ├── index.tsx
│ │ │ │ ├── profile
│ │ │ │ ├── _components
│ │ │ │ │ ├── VolunteerRecruitments.tsx
│ │ │ │ │ └── VolunteerReviews.tsx
│ │ │ │ └── index.tsx
│ │ │ │ ├── search
│ │ │ │ ├── _components
│ │ │ │ │ └── RecruitmentsSearchFilter.tsx
│ │ │ │ ├── _constants
│ │ │ │ │ └── filter.ts
│ │ │ │ ├── _hooks
│ │ │ │ │ └── useRecruitmentSearch.ts
│ │ │ │ ├── _types
│ │ │ │ │ └── filter.ts
│ │ │ │ └── index.tsx
│ │ │ │ ├── update
│ │ │ │ └── index.tsx
│ │ │ │ └── write
│ │ │ │ └── index.tsx
│ │ ├── react-query.d.ts
│ │ ├── routes
│ │ │ └── index.tsx
│ │ ├── types
│ │ │ ├── apis
│ │ │ │ ├── auth.ts
│ │ │ │ ├── recruitment.ts
│ │ │ │ ├── shetler.ts
│ │ │ │ └── volunteers.ts
│ │ │ └── recruitment.ts
│ │ ├── utils
│ │ │ └── test
│ │ │ │ ├── render.tsx
│ │ │ │ └── setupTests.ts
│ │ └── vite-env.d.ts
│ ├── tsconfig.json
│ ├── vercel.json
│ └── vite.config.ts
└── volunteer
│ ├── .eslintrc.cjs
│ ├── .gitignore
│ ├── README.md
│ ├── index.html
│ ├── package.json
│ ├── public
│ ├── favicon.ico
│ ├── mockServiceWorker.js
│ └── vite.svg
│ ├── src
│ ├── App.tsx
│ ├── apis
│ │ ├── animal.ts
│ │ ├── auth.ts
│ │ ├── recruitment.ts
│ │ ├── review.ts
│ │ ├── shelter.ts
│ │ └── volunteer.ts
│ ├── assets
│ │ └── react.svg
│ ├── constants
│ │ ├── applicantStatus.ts
│ │ └── path.ts
│ ├── main.tsx
│ ├── mocks
│ │ ├── browser.ts
│ │ └── handlers
│ │ │ ├── auth.ts
│ │ │ ├── image.ts
│ │ │ ├── recruitment.ts
│ │ │ ├── review.ts
│ │ │ ├── shelter.ts
│ │ │ └── volunteer.ts
│ ├── pages
│ │ ├── animals
│ │ │ ├── detail
│ │ │ │ └── index.tsx
│ │ │ └── index.tsx
│ │ ├── chattings
│ │ │ ├── index.tsx
│ │ │ └── room
│ │ │ │ └── index.tsx
│ │ ├── my
│ │ │ ├── _components
│ │ │ │ ├── ApplyRecruitments.tsx
│ │ │ │ └── MyReviews.tsx
│ │ │ ├── _hooks
│ │ │ │ └── useFetchMyVolunteer.ts
│ │ │ └── index.tsx
│ │ ├── notfound
│ │ │ └── index.tsx
│ │ ├── notifications
│ │ │ └── index.tsx
│ │ ├── settings
│ │ │ ├── account
│ │ │ │ └── index.tsx
│ │ │ ├── index.tsx
│ │ │ └── password
│ │ │ │ └── index.tsx
│ │ ├── shelters
│ │ │ ├── profile
│ │ │ │ ├── _components
│ │ │ │ │ ├── ShelterRecruitments.tsx
│ │ │ │ │ └── ShelterReviews.tsx
│ │ │ │ └── index.tsx
│ │ │ └── reviews
│ │ │ │ ├── _components
│ │ │ │ └── ReviewSubmitButton.tsx
│ │ │ │ ├── _constants
│ │ │ │ └── reviews.ts
│ │ │ │ ├── _schema
│ │ │ │ └── reviewSchema.ts
│ │ │ │ ├── update
│ │ │ │ └── index.tsx
│ │ │ │ └── write
│ │ │ │ └── index.tsx
│ │ ├── signin
│ │ │ └── index.tsx
│ │ ├── signup
│ │ │ └── index.tsx
│ │ └── volunteers
│ │ │ ├── _components
│ │ │ ├── RecruitSkeleton.tsx
│ │ │ ├── RecruitSkeletonList.tsx
│ │ │ └── VolunteerRecruitItem.tsx
│ │ │ ├── _queryOptions
│ │ │ └── recruitments.ts
│ │ │ ├── _utils
│ │ │ └── recruitment.ts
│ │ │ ├── detail
│ │ │ ├── _hooks
│ │ │ │ ├── useFetchRecruitmentDetail.ts
│ │ │ │ └── useFetchSimpleShelterInfo.ts
│ │ │ └── index.tsx
│ │ │ ├── index.tsx
│ │ │ └── search
│ │ │ ├── _components
│ │ │ └── RecruitmentsSearchFilter.tsx
│ │ │ ├── _constants
│ │ │ └── filter.ts
│ │ │ ├── _hooks
│ │ │ └── useRecruitmentSearch.ts
│ │ │ ├── _types
│ │ │ └── filter.ts
│ │ │ └── index.tsx
│ ├── react-query.d.ts
│ ├── routes
│ │ └── index.tsx
│ ├── types
│ │ └── apis
│ │ │ ├── auth.ts
│ │ │ ├── recruitment.ts
│ │ │ ├── review.ts
│ │ │ └── volunteer.ts
│ ├── utils
│ │ └── test
│ │ │ ├── render.tsx
│ │ │ └── setupTests.ts
│ └── vite-env.d.ts
│ ├── tsconfig.json
│ ├── vercel.json
│ └── vite.config.ts
├── commitlint.config.js
├── configs
├── eslint-config-anifriends
│ ├── index.js
│ └── package.json
└── tsconfig
│ ├── base.json
│ ├── package.json
│ ├── react-library.json
│ └── vite.json
├── cz-config.js
├── package.json
├── packages
├── apis
│ ├── package.json
│ ├── src
│ │ ├── axiosInstance.ts
│ │ ├── axiosInterceptor.ts
│ │ ├── common
│ │ │ ├── accessToken.ts
│ │ │ ├── image.ts
│ │ │ ├── index.ts
│ │ │ ├── recruitments.ts
│ │ │ ├── review.ts
│ │ │ └── volunteer.ts
│ │ └── index.ts
│ └── tsconfig.json
├── assets
│ ├── bottomNavBar
│ │ ├── icon_animals_selected.svg
│ │ ├── icon_animals_unselected.svg
│ │ ├── icon_chattings_selected.svg
│ │ ├── icon_chattings_unselected.svg
│ │ ├── icon_mypage_selected.svg
│ │ ├── icon_mypage_unselected.svg
│ │ ├── icon_volunteers_selected.svg
│ │ └── icon_volunteers_unselected.svg
│ ├── icon-IoEyeOff.svg
│ ├── icon-IoEyeSharp.svg
│ ├── icon_BiX.svg
│ ├── icon_IoCamera.svg
│ ├── icon_applicant.svg
│ ├── icon_back.svg
│ ├── icon_menu.svg
│ ├── icon_next.svg
│ ├── icon_no_image.svg
│ ├── icon_notifications.svg
│ ├── icon_review_next.svg
│ ├── icon_search.svg
│ ├── icon_settings.svg
│ ├── image-anifriends-logo.png
│ ├── package.json
│ ├── src
│ │ ├── ApplicantIcon.tsx
│ │ └── index.tsx
│ └── tsconfig.json
├── components
│ ├── package.json
│ ├── png.d.ts
│ ├── src
│ │ ├── AlertModal.tsx
│ │ ├── ApplicantStatus.tsx
│ │ ├── EditPhotoItem.tsx
│ │ ├── EditPhotoList.tsx
│ │ ├── FilterGroup.tsx
│ │ ├── FilterSelect.tsx
│ │ ├── ImageCarousel.tsx
│ │ ├── InfoItem.tsx
│ │ ├── InfoList.tsx
│ │ ├── InfoSubtext.tsx
│ │ ├── InfoTextItem.tsx
│ │ ├── InfoTextList.tsx
│ │ ├── Label.tsx
│ │ ├── LabelText.tsx
│ │ ├── Loader.tsx
│ │ ├── LocalErrorBoundary.tsx
│ │ ├── LogoImageBox.tsx
│ │ ├── NotReady.tsx
│ │ ├── OptionMenu.tsx
│ │ ├── ProfileInfo.tsx
│ │ ├── RadioGroup.tsx
│ │ ├── ReviewItem.tsx
│ │ ├── ReviewItemSkeleton.tsx
│ │ ├── ReviewItemSkeletonList.tsx
│ │ ├── SearchFilters.tsx
│ │ ├── SettingGroup.tsx
│ │ ├── SettingItem.tsx
│ │ ├── Tabs.tsx
│ │ ├── WithLogin.tsx
│ │ └── index.tsx
│ ├── svg.d.ts
│ └── tsconfig.json
├── constants
│ ├── package.json
│ ├── src
│ │ ├── appType.ts
│ │ ├── baseURL.ts
│ │ ├── date.ts
│ │ ├── gender.ts
│ │ ├── headerTitle.ts
│ │ ├── headerType.ts
│ │ ├── index.ts
│ │ ├── pageType.ts
│ │ └── period.ts
│ └── tsconfig.json
├── fonts
│ ├── package.json
│ ├── src
│ │ └── index.tsx
│ └── tsconfig.json
├── hooks
│ ├── package.json
│ ├── src
│ │ ├── index.ts
│ │ ├── useAccessTokenMutation.ts
│ │ ├── useIntersect.ts
│ │ ├── usePageType.ts
│ │ ├── usePhotoUpload.ts
│ │ ├── usePhotosUpload.ts
│ │ ├── useRadioGroup.ts
│ │ ├── useSearchFilter.ts
│ │ ├── useSearchKeyword.ts
│ │ └── useToggle.ts
│ └── tsconfig.json
├── icons
│ ├── package.json
│ ├── src
│ │ ├── ApplicantIcon.tsx
│ │ ├── BackIcon.tsx
│ │ ├── BiXIcon.tsx
│ │ ├── IoCameraIcon.tsx
│ │ ├── IoEyeOff.tsx
│ │ ├── IoEyeSharp.tsx
│ │ ├── MenuIcon.tsx
│ │ ├── NextIcon.tsx
│ │ ├── NoImageIcon.tsx
│ │ ├── NotificationIcon.tsx
│ │ ├── ReviewNextIcon.tsx
│ │ ├── SearchIcon.tsx
│ │ ├── SettingsIcon.tsx
│ │ └── index.tsx
│ └── tsconfig.json
├── layout
│ ├── package.json
│ ├── src
│ │ ├── BottomNavBar
│ │ │ ├── NavBarButton.tsx
│ │ │ ├── index.tsx
│ │ │ └── useBottomNavBar.ts
│ │ ├── Header
│ │ │ ├── DefaultHeader
│ │ │ │ ├── headerIconState.ts
│ │ │ │ ├── index.tsx
│ │ │ │ └── useDefaultHeader.ts
│ │ │ ├── DetailHeader
│ │ │ │ ├── index.tsx
│ │ │ │ └── useDetailHeader.ts
│ │ │ ├── SearchHeader
│ │ │ │ └── index.tsx
│ │ │ ├── index.tsx
│ │ │ ├── useHeader.ts
│ │ │ └── utils.ts
│ │ └── index.tsx
│ ├── svg.d.ts
│ └── tsconfig.json
├── store
│ ├── package.json
│ ├── src
│ │ ├── authStore.ts
│ │ ├── detailHeaderStore.ts
│ │ ├── index.ts
│ │ └── searchHeaderStore.ts
│ └── tsconfig.json
├── theme
│ ├── package.json
│ ├── src
│ │ └── index.ts
│ └── tsconfig.json
├── types
│ ├── package.json
│ ├── src
│ │ ├── apis
│ │ │ ├── auth.ts
│ │ │ ├── error.ts
│ │ │ └── index.ts
│ │ ├── app.ts
│ │ ├── gender.ts
│ │ ├── header.ts
│ │ ├── index.ts
│ │ ├── page.ts
│ │ └── period.ts
│ └── tsconfig.json
└── utils
│ ├── package.json
│ ├── src
│ ├── date.ts
│ ├── errorMessage.ts
│ ├── image.ts
│ ├── index.ts
│ ├── localStorage.ts
│ ├── period.ts
│ ├── toast.ts
│ └── validations.ts
│ └── tsconfig.json
├── packlint.config.mjs
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
└── turbo.json
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | extends: ['anifriends'],
4 | };
5 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/♻️-리팩토링-요청.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: "♻️ 리팩토링 요청"
3 | about: 변경 혹은 개선해야 되는 문제를 작성해 주세요
4 | title: 'refactor: '
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | ## 개선해야 되는 코드 혹은 기능에 대해서 적어주세요
11 |
12 |
13 |
14 |
15 | ## 원하는 개선 방향
16 |
17 |
18 |
19 |
20 | ## 생각 중인 기능 추가 방안
21 |
22 |
23 |
24 |
25 | ## ETC
26 |
27 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/✨-기능-추가-요청.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: "✨ 기능 추가 요청"
3 | about: 구현하려는 새로운 기능을 요청
4 | title: 'feat: '
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | ## 추가하려는 기능이 어떠한 문제 혹은 기능과 연관되어 있나요?
11 |
12 |
13 |
14 |
15 | ## 원하는 기능 추가
16 |
17 |
18 | - [ ] todo
19 | - [ ] todo
20 |
21 | ## 생각 중인 기능 추가 방안
22 |
23 |
24 |
25 |
26 | ## ETC
27 |
28 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/🐛-버그-리포트.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: "\U0001F41B 버그 리포트"
3 | about: 버그를 고쳐주세요
4 | title: 'bug: '
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | ## 버그 설명
11 |
12 |
13 |
14 |
15 | ## 버그 발생 단계
16 |
24 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | ## 📌 이슈번호
2 |
3 | - close #이슈번호
4 |
5 | ## 📗 구현 내용
6 |
7 | ## 📖 구현 스크린샷
8 |
9 | ## 📖 PR 포인트 & 궁금한 점
10 |
--------------------------------------------------------------------------------
/.github/workflows/build-test.yml:
--------------------------------------------------------------------------------
1 | name: Build Test
2 |
3 | on:
4 | push:
5 | branches: [main, develop]
6 | pull_request:
7 | branches: [main, develop]
8 | workflow_dispatch:
9 |
10 | jobs:
11 | build:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - name: Checkout repository
15 | uses: actions/checkout@v3
16 |
17 | - name: Setup Node.js
18 | uses: actions/setup-node@v3
19 | with:
20 | node-version: '20.9.0'
21 |
22 | - name: Install pnpm
23 | uses: pnpm/action-setup@v2
24 | with:
25 | version: 8
26 | run_install: false
27 |
28 | - name: Get pnpm store directory
29 | id: pnpm-cache
30 | shell: bash
31 | run: |
32 | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
33 |
34 | - name: Setup pnpm cache
35 | uses: actions/cache@v3
36 | with:
37 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
38 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
39 | restore-keys: |
40 | ${{ runner.os }}-pnpm-store-
41 |
42 | - name: Install dependencies
43 | run: pnpm install
44 |
45 | - name: Build React app
46 | run: pnpm build
47 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | .turbo
4 | *.log
5 | dist
6 | dist-ssr
7 | *.local
8 | .env
9 | .cache
10 | server/dist
11 | public/dist
12 |
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | pnpm commitlint --edit $1
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | pnpm lint
5 | pnpm lint-staged
6 |
--------------------------------------------------------------------------------
/.husky/pre-push:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | pnpm build
5 |
--------------------------------------------------------------------------------
/.lintstagedrc:
--------------------------------------------------------------------------------
1 | {
2 | "*.{ts,tsx}": ["eslint --fix"]
3 | }
4 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | auto-install-peers = true
2 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | v20.9.0
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "arrowParens": "always",
3 | "bracketSameLine": false,
4 | "bracketSpacing": true,
5 | "endOfLine": "auto",
6 | "jsxSingleQuote": false,
7 | "printWidth": 80,
8 | "semi": true,
9 | "singleAttributePerLine": false,
10 | "singleQuote": true,
11 | "tabWidth": 2,
12 | "trailingComma": "all",
13 | "useTabs": false
14 | }
15 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "dbaeumer.vscode-eslint",
4 | "esbenp.prettier-vscode",
5 | "eamodio.gitlens",
6 | "formulahendry.auto-rename-tag"
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "eslint.workingDirectories": [
3 | {
4 | "mode": "auto"
5 | }
6 | ],
7 | "editor.codeActionsOnSave": {
8 | "source.fixAll": "explicit"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/apps/shelter/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | extends: ['anifriends'],
4 | overrides: [
5 | {
6 | files: ['__mocks__/**.ts', 'src/**/*.spec.ts', 'src/**/*.spec.tsx'],
7 | plugins: ['vitest'],
8 | extends: ['plugin:vitest/recommended'],
9 | rules: {
10 | 'vitest/expect-expect': 'off',
11 | },
12 | globals: {
13 | globalThis: true,
14 | describe: true,
15 | it: true,
16 | expect: true,
17 | beforeEach: true,
18 | afterEach: true,
19 | beforeAll: true,
20 | afterAll: true,
21 | vi: true,
22 | },
23 | },
24 | ],
25 | };
26 |
--------------------------------------------------------------------------------
/apps/shelter/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/apps/shelter/README.md:
--------------------------------------------------------------------------------
1 | # React + TypeScript + Vite
2 |
3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4 |
5 | Currently, two official plugins are available:
6 |
7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9 |
10 | ## Expanding the ESLint configuration
11 |
12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
13 |
14 | - Configure the top-level `parserOptions` property like this:
15 |
16 | ```js
17 | parserOptions: {
18 | ecmaVersion: 'latest',
19 | sourceType: 'module',
20 | project: ['./tsconfig.json', './tsconfig.node.json'],
21 | tsconfigRootDir: __dirname,
22 | },
23 | ```
24 |
25 | - Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
26 | - Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
27 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list
28 |
--------------------------------------------------------------------------------
/apps/shelter/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
13 |
14 | Anifriends
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/apps/shelter/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anifriends/frontend/8b346b0db582ab9f5894e83c133762b8bdbeae9e/apps/shelter/public/favicon.ico
--------------------------------------------------------------------------------
/apps/shelter/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/shelter/src/App.tsx:
--------------------------------------------------------------------------------
1 | import Fonts from '@anifriends/fonts';
2 | import theme from '@anifriends/theme';
3 | import { ChakraProvider } from '@chakra-ui/react';
4 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
5 | import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
6 | import { RouterProvider } from 'react-router-dom';
7 |
8 | import { router } from '@/routes';
9 |
10 | const queryClient = new QueryClient({
11 | defaultOptions: {
12 | queries: {
13 | staleTime: 50 * 1000,
14 | retry: false,
15 | },
16 | },
17 | });
18 |
19 | export default function App() {
20 | return (
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/apps/shelter/src/apis/auth.ts:
--------------------------------------------------------------------------------
1 | import { axiosInstance } from '@anifriends/apis';
2 | import type {
3 | ChangePasswordRequestData,
4 | CheckDuplicatedEmailRequestData,
5 | CheckDuplicatedEmailResponseData,
6 | SigninRequestData,
7 | SigninResponseData,
8 | } from '@anifriends/types';
9 |
10 | import { SignupRequestData } from '@/types/apis/auth';
11 |
12 | export const signinShelter = (data: SigninRequestData) =>
13 | axiosInstance.post(
14 | '/auth/shelters/login',
15 | data,
16 | );
17 |
18 | export const signupShelter = (data: SignupRequestData) =>
19 | axiosInstance.post('/shelters', data);
20 |
21 | export const checkDuplicatedShelterEmail = (
22 | data: CheckDuplicatedEmailRequestData,
23 | ) =>
24 | axiosInstance.post<
25 | CheckDuplicatedEmailResponseData,
26 | CheckDuplicatedEmailRequestData
27 | >('/shelters/email', data);
28 |
29 | export const changeShelterPassword = (data: ChangePasswordRequestData) =>
30 | axiosInstance.patch(
31 | '/shelters/me/passwords',
32 | data,
33 | );
34 |
--------------------------------------------------------------------------------
/apps/shelter/src/apis/shelter.ts:
--------------------------------------------------------------------------------
1 | import { axiosInstance } from '@anifriends/apis';
2 |
3 | import { ShelterInfo, UpdateShelterInfo } from '@/types/apis/shetler';
4 |
5 | type PasswordUpdateParams = {
6 | newPassword: string;
7 | oldPassword: string;
8 | };
9 |
10 | type PageParams = {
11 | page: number;
12 | size: number;
13 | };
14 |
15 | export const getShelterInfoAPI = () =>
16 | axiosInstance.get('/shelters/me');
17 |
18 | export const updateShelterInfo = (shelterInfo: UpdateShelterInfo) =>
19 | axiosInstance.patch('/shelters/me', shelterInfo);
20 |
21 | export const updatePassword = (passwordUpdateParams: PasswordUpdateParams) =>
22 | axiosInstance.patch(
23 | '/shelters/me/password',
24 | passwordUpdateParams,
25 | );
26 |
27 | export const updateAddressStatusAPI = (isOpenedAddress: boolean) =>
28 | axiosInstance.patch<
29 | unknown,
30 | {
31 | isOpenedAddress: boolean;
32 | }
33 | >('/shelters/me/address/status', { isOpenedAddress });
34 |
35 | export const getShelterReviewList = (pageParams: PageParams) =>
36 | axiosInstance.get<{
37 | pageInfo: {
38 | totalElements: number;
39 | hasNext: boolean;
40 | };
41 | reviews: {
42 | reviewId: number;
43 | reviewCreatedAt: string;
44 | reviewContent: string;
45 | reviewImageUrls: string[];
46 | volunteerName: string;
47 | volunteerTemperature: number;
48 | volunteerReviewCount: number;
49 | volunteerImageUrl: string;
50 | volunteerId: number;
51 | }[];
52 | }>(`/shelters/me/reviews`, {
53 | params: pageParams,
54 | });
55 |
--------------------------------------------------------------------------------
/apps/shelter/src/apis/volunteers.ts:
--------------------------------------------------------------------------------
1 | import { axiosInstance } from '@anifriends/apis';
2 |
3 | import {
4 | VolunteerCompletedsRequestParams,
5 | VolunteerProfileResponseData,
6 | VolunteerReviewsOnVolunteerResponseData,
7 | VoluteerRecruitmentsOnVolunteerResponseData,
8 | } from '@/types/apis/volunteers';
9 |
10 | export const getVolunteerProfile = (volunteerId: number) =>
11 | axiosInstance.get(
12 | `/shelters/volunteers/${volunteerId}/profile`,
13 | );
14 |
15 | export const getVolunteerReviewsOnVolunteer = (
16 | volunteerId: number,
17 | params: VolunteerCompletedsRequestParams,
18 | ) =>
19 | axiosInstance.get(
20 | `/shelters/volunteers/${volunteerId}/reviews`,
21 | { params },
22 | );
23 |
24 | export const getVolunteerRecruitmentsOnVolunteer = (
25 | volunteerId: number,
26 | params: VolunteerCompletedsRequestParams,
27 | ) =>
28 | axiosInstance.get(
29 | `/shelters/volunteers/${volunteerId}/recruitments/completed`,
30 | { params },
31 | );
32 |
--------------------------------------------------------------------------------
/apps/shelter/src/assets/CkCheck.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentProps } from 'react';
2 |
3 | export default function CkCheck({ ...props }: ComponentProps<'svg'>) {
4 | return (
5 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/apps/shelter/src/assets/CkClose.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentProps } from 'react';
2 |
3 | export default function CkClose({ ...props }: ComponentProps<'svg'>) {
4 | return (
5 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/apps/shelter/src/constants/path.ts:
--------------------------------------------------------------------------------
1 | const PATH = {
2 | VOLUNTEERS: {
3 | INDEX: 'volunteers',
4 | DETAIL: ':id',
5 | PROFILE: 'profile/:id',
6 | SEARCH: 'search',
7 | WRITE: 'write',
8 | UPDATE: 'write/:id',
9 | },
10 | ANIMALS: {
11 | INDEX: 'animals',
12 | DETAIL: ':id',
13 | SEARCH: 'search',
14 | WRITE: 'write',
15 | UPDATE: 'write/:id',
16 | },
17 | CHATTINGS: {
18 | INDEX: 'chattings',
19 | ROOM: 'chattings/:id',
20 | },
21 | MYPAGE: {
22 | INDEX: 'mypage',
23 | REVIEWS: 'reviews',
24 | },
25 | SETTINGS: {
26 | INDEX: 'settings',
27 | ACCOUNT: 'account',
28 | PASSWORD: 'password',
29 | },
30 | MANAGE: {
31 | INDEX: 'manage',
32 | ATTENDANCE: 'attendance/:id',
33 | APPLY: 'apply/:id',
34 | },
35 | NOTIFICATIONS: 'notifications',
36 | SIGNUP: 'signup',
37 | SIGNIN: 'signin',
38 | };
39 |
40 | export default PATH;
41 |
--------------------------------------------------------------------------------
/apps/shelter/src/constants/recruitment.ts:
--------------------------------------------------------------------------------
1 | export const APPLICANT_STATUS_ENG = {
2 | PENDING: 'PENDING',
3 | APPROVED: 'APPROVED',
4 | REFUSED: 'REFUSED',
5 | } as const;
6 |
7 | export const APPLICANT_STATUS_KOR = {
8 | PENDING: '대기중',
9 | APPROVED: '승인됨',
10 | APPROVE: '승인',
11 | REFUSED: '거절됨',
12 | REFUSE: '거절',
13 | } as const;
14 |
--------------------------------------------------------------------------------
/apps/shelter/src/main.tsx:
--------------------------------------------------------------------------------
1 | import ReactDOM from 'react-dom/client';
2 |
3 | import App from './App.tsx';
4 |
5 | async function deferRender() {
6 | if (import.meta.env.MODE !== 'development') {
7 | return;
8 | }
9 |
10 | const { worker } = await import('./mocks/browser.ts');
11 |
12 | // `worker.start()` returns a Promise that resolves
13 | // once the Service Worker is up and ready to intercept requests.
14 | return worker.start();
15 | }
16 |
17 | deferRender().then(() => {
18 | ReactDOM.createRoot(document.getElementById('root')!).render();
19 | });
20 |
--------------------------------------------------------------------------------
/apps/shelter/src/mocks/browser.ts:
--------------------------------------------------------------------------------
1 | import { setupWorker } from 'msw/browser';
2 |
3 | import { handlers as authHandlers } from './handlers/auth';
4 | import { handlers as imageHandlers } from './handlers/image';
5 | import { handlers as manageHandlers } from './handlers/manage';
6 | import { handlers as recruitmentHandler } from './handlers/recruitment';
7 | import { handlers as recruitmentDetailHandler } from './handlers/recruitmentDetail';
8 | import { handlers as shelterHandlers } from './handlers/shelter';
9 | import { handlers as volunteerHandlers } from './handlers/volunteers';
10 |
11 | export const worker = setupWorker(
12 | ...authHandlers,
13 | ...imageHandlers,
14 | ...manageHandlers,
15 | ...recruitmentHandler,
16 | ...recruitmentDetailHandler,
17 | ...shelterHandlers,
18 | ...volunteerHandlers,
19 | );
20 |
--------------------------------------------------------------------------------
/apps/shelter/src/mocks/handlers/image.ts:
--------------------------------------------------------------------------------
1 | import { delay, http, HttpResponse } from 'msw';
2 |
3 | export const handlers = [
4 | http.post('/images', async () => {
5 | await delay(200);
6 |
7 | return HttpResponse.json(
8 | {
9 | imageUrls: ['https://source.unsplash.com/random'],
10 | },
11 | { status: 200 },
12 | );
13 | }),
14 | ];
15 |
--------------------------------------------------------------------------------
/apps/shelter/src/mocks/handlers/manage.ts:
--------------------------------------------------------------------------------
1 | import { delay, http, HttpResponse } from 'msw';
2 |
3 | const DUMMY_USER = {
4 | volunteerId: 1,
5 | applicantId: 2,
6 | volunteerName: '김영희',
7 | volunteerBirthDate: '2021-11-08',
8 | volunteerGender: 'FEMALE',
9 | volunteerPhoneNumber: '010-1234-5678',
10 | volunteerAttendance: false,
11 | };
12 |
13 | const DUMMY_USER_LIST = Array.from({ length: 20 }, () => {
14 | return {
15 | ...DUMMY_USER,
16 | volunteerId: Math.random(),
17 | applicantId: Math.random(),
18 | };
19 | });
20 |
21 | export const handlers = [
22 | http.get('/shelters/recruitments/:recruitmentId/approval', async () => {
23 | await delay(200);
24 | return HttpResponse.json(
25 | {
26 | applicants: DUMMY_USER_LIST,
27 | },
28 | { status: 200 },
29 | );
30 | }),
31 | http.patch('/shelters/recruitments/:recruitmentId/approval', async () => {
32 | await delay(1000);
33 | return HttpResponse.json({ status: 200 });
34 | }),
35 | ];
36 |
--------------------------------------------------------------------------------
/apps/shelter/src/mocks/handlers/recruitmentDetail.ts:
--------------------------------------------------------------------------------
1 | import { delay, http, HttpResponse } from 'msw';
2 |
3 | export const handlers = [
4 | http.get('/recruitments/:id', async () => {
5 | await delay(200);
6 | return HttpResponse.json(
7 | {
8 | recruitmentTitle: '가짜 데이터 제목입니다.',
9 | recruitmentApplicantCount: 5,
10 | recruitmentCapacity: 10,
11 | recruitmentContent: '가짜 데이터 내용입니다!!! '.repeat(50),
12 | recruitmentStartTime: '2023-12-17T14:00:00',
13 | recruitmentEndTime: '2023-12-17T16:00:00',
14 | recruitmentIsClosed: false,
15 | recruitmentDeadline: '2023-12-18T18:00:00',
16 | recruitmentCreatedAt: '2023-12-15T14:00:00',
17 | recruitmentUpdatedAt: '2023-12-15T14:00:00',
18 | recruitmentImageUrls: [
19 | 'https://source.unsplash.com/random/?animal',
20 | 'https://source.unsplash.com/random/300X500',
21 | ],
22 | },
23 | { status: 200 },
24 | );
25 | }),
26 | ];
27 |
--------------------------------------------------------------------------------
/apps/shelter/src/mocks/handlers/shelter.ts:
--------------------------------------------------------------------------------
1 | import { delay, http, HttpResponse } from 'msw';
2 |
3 | const DUMMY_IMAGE = 'https://source.unsplash.com/random';
4 | const DUMMY_IMAGE_LIST = Array.from({ length: 4 }, () => DUMMY_IMAGE);
5 | const DUMMY_REVIEW = {
6 | reviewId: 32,
7 | reviewCreatedAt: '2023-03-16T18:00',
8 | reviewContent: '시설이 너무 깨끗하고 강아지도...',
9 | reviewImageUrls: DUMMY_IMAGE_LIST,
10 | volunteerName: '강혜린',
11 | volunteerTemperature: 44,
12 | volunteerReviewCount: 4,
13 | volunteerImageUrl: DUMMY_IMAGE,
14 | };
15 | const DUMMY_REVIEW_LIST = Array.from({ length: 4 }, () => DUMMY_REVIEW);
16 |
17 | export const handlers = [
18 | http.get('/shelters/me', async () => {
19 | await delay(200);
20 | return HttpResponse.json(
21 | {
22 | shelterId: 1,
23 | shelterEmail: 'Shelter1234@gmail.com',
24 | shelterName: '양천구 보호소',
25 | shelterImageUrl: 'https://source.unsplash.com/random/?animal',
26 | shelterAddress: '서울특별시 양천구',
27 | shelterAddressDetail: '서울특별시 양천구 신월동 동자빌딩',
28 | shelterPhoneNumber: '010-1234-5678',
29 | shelterSparePhoneNumber: '02-345-6780',
30 | shelterIsOpenedAddress: true,
31 | },
32 | { status: 200 },
33 | );
34 | }),
35 | http.patch('/shelters/me/address/status', async () => {
36 | await delay(200);
37 | return HttpResponse.json({ status: 200 });
38 | }),
39 | http.get('/shelters/me/reviews', async () => {
40 | await delay(3000);
41 | return HttpResponse.json(
42 | {
43 | pageInfo: {
44 | totalElements: 100,
45 | hasNext: true,
46 | },
47 | reviews: DUMMY_REVIEW_LIST,
48 | },
49 | { status: 200 },
50 | );
51 | }),
52 | http.patch('/shelters/me', async () => {
53 | await delay(200);
54 | return new HttpResponse(null, { status: 204 });
55 | }),
56 | ];
57 |
--------------------------------------------------------------------------------
/apps/shelter/src/mocks/handlers/volunteers.ts:
--------------------------------------------------------------------------------
1 | import { delay, http, HttpResponse } from 'msw';
2 |
3 | const DUMMY_REVIEWS_DATA = {
4 | reviewId: 36,
5 | shelterName: '남양주 보호소',
6 | reviewCreatedAt: '2023-03-16T18:00',
7 | reviewContent: '아이들이 너무 귀여워서 봉사하는 시간이 즐거웠습니다~!',
8 | reviewImageUrls: [
9 | 'https://source.unsplash.com/random',
10 | 'https://source.unsplash.com/random',
11 | 'https://source.unsplash.com/random',
12 | ],
13 | };
14 |
15 | const DUMMY_RECRUITMENTS = {
16 | recruitmentId: 1,
17 | recruitmentTitle: '봉사자를 모집합니다',
18 | recruitmentStartTime: '2023-03-16T18:00:00',
19 | shelterName: '마석 보호소',
20 | };
21 |
22 | export const handlers = [
23 | http.get('/shelters/volunteers/:volunteerId/profile', async () => {
24 | await delay(200);
25 | return HttpResponse.json(
26 | {
27 | volunteerEmail: 'test@naver.com',
28 | volunteerName: '홍길동',
29 | volunteerTemperature: 36,
30 | volunteerImageUrl: 'https://source.unsplash.com/random',
31 | volunteerPhoneNumber: '010-8237-1847',
32 | },
33 | { status: 200 },
34 | );
35 | }),
36 | http.get('/shelters/volunteers/:volunteerId/reviews', async () => {
37 | await delay(2000);
38 | return HttpResponse.json(
39 | {
40 | pageInfo: {
41 | totalElements: 20,
42 | hasNext: true,
43 | },
44 | reviews: Array.from({ length: 10 }, () => DUMMY_REVIEWS_DATA),
45 | },
46 | { status: 200 },
47 | );
48 | }),
49 | http.get(
50 | '/shelters/volunteers/:volunteerId/recruitments/completed',
51 | async () => {
52 | await delay(2000);
53 | return HttpResponse.json({
54 | pageInfo: {
55 | totalElements: 20,
56 | hasNext: true,
57 | },
58 | recruitments: Array.from({ length: 10 }, () => DUMMY_RECRUITMENTS),
59 | });
60 | },
61 | ),
62 | ];
63 |
--------------------------------------------------------------------------------
/apps/shelter/src/pages/animals/detail/index.tsx:
--------------------------------------------------------------------------------
1 | import { useDetailHeaderStore } from '@anifriends/store';
2 | import { useEffect } from 'react';
3 |
4 | const handleDeletePost = (postId: number) => {
5 | // TODO: AnimalPost delete API 호출
6 | console.log('[Delete Animal] postId:', postId);
7 | };
8 |
9 | export default function AnimalsDetailPage() {
10 | const setOnDelete = useDetailHeaderStore((state) => state.setOnDelete);
11 |
12 | useEffect(() => {
13 | setOnDelete(handleDeletePost);
14 |
15 | return () => {
16 | setOnDelete(() => {});
17 | };
18 | }, [setOnDelete]);
19 |
20 | return AnimalsDetailPage
;
21 | }
22 |
--------------------------------------------------------------------------------
/apps/shelter/src/pages/animals/index.tsx:
--------------------------------------------------------------------------------
1 | import { NotReady } from '@anifriends/components';
2 |
3 | export default function AnimalsPage() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/apps/shelter/src/pages/animals/search/index.tsx:
--------------------------------------------------------------------------------
1 | import { useSearchHeaderStore } from '@anifriends/store';
2 | import { useEffect } from 'react';
3 |
4 | const handleSearchkeyword = (keyword: string) => {
5 | // TODO: AnimalList 검색 API 호출
6 | console.log('[Search Animal] - keyword:', keyword);
7 | };
8 |
9 | export default function AnimalsSearchPage() {
10 | const setOnSearch = useSearchHeaderStore((state) => state.setOnSearch);
11 |
12 | useEffect(() => {
13 | setOnSearch(handleSearchkeyword);
14 |
15 | return () => {
16 | setOnSearch(() => {});
17 | };
18 | }, [setOnSearch]);
19 |
20 | return AnimalsSearchPage
;
21 | }
22 |
--------------------------------------------------------------------------------
/apps/shelter/src/pages/animals/update/index.tsx:
--------------------------------------------------------------------------------
1 | export default function AnimalsUpdatePage() {
2 | return AnimalsUpdatePage
;
3 | }
4 |
--------------------------------------------------------------------------------
/apps/shelter/src/pages/animals/write/index.tsx:
--------------------------------------------------------------------------------
1 | export default function AnimalsWritePage() {
2 | return AnimalsWritePage
;
3 | }
4 |
--------------------------------------------------------------------------------
/apps/shelter/src/pages/chattings/index.tsx:
--------------------------------------------------------------------------------
1 | import { NotReady } from '@anifriends/components';
2 |
3 | export default function ChattingsPage() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/apps/shelter/src/pages/chattings/room/index.tsx:
--------------------------------------------------------------------------------
1 | export default function ChattingsRoomPage() {
2 | return ChattingsRoomPage
;
3 | }
4 |
--------------------------------------------------------------------------------
/apps/shelter/src/pages/manage/apply/_components/ApplyInfoItem.tsx:
--------------------------------------------------------------------------------
1 | import { ApplicantStatus } from '@anifriends/components';
2 | import { Flex, Text } from '@chakra-ui/react';
3 |
4 | type ApplyInfoItemProps = {
5 | currentRecuritmentCount: number;
6 | recruitmentCapacity: number;
7 | };
8 |
9 | export default function ApplyInfoBox({
10 | currentRecuritmentCount,
11 | recruitmentCapacity,
12 | }: ApplyInfoItemProps) {
13 | return (
14 |
24 |
25 | 총{' '}
26 | {`${currentRecuritmentCount}명`}
30 | 이 봉사를 신청했습니다
31 |
32 |
36 |
37 | );
38 | }
39 |
--------------------------------------------------------------------------------
/apps/shelter/src/pages/manage/apply/_components/ApprovedCountBox.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Button } from '@chakra-ui/react';
2 |
3 | import CkCheck from '@/assets/CkCheck';
4 |
5 | type ApprovedCountBoxProps = {
6 | approvedCount: number;
7 | };
8 |
9 | export default function ApprovedCountBox({
10 | approvedCount,
11 | }: ApprovedCountBoxProps) {
12 | return (
13 | }
21 | bgColor="orange.400"
22 | color="white"
23 | borderRadius={50}
24 | _hover={{ bg: undefined }}
25 | _active={{ bg: undefined }}
26 | >
27 | {`${approvedCount}명 승인됨`}
28 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/apps/shelter/src/pages/manage/apply/index.tsx:
--------------------------------------------------------------------------------
1 | import { Box } from '@chakra-ui/react';
2 | import { useSuspenseQuery } from '@tanstack/react-query';
3 | import { Suspense } from 'react';
4 | import { useParams } from 'react-router-dom';
5 |
6 | import { getShelterRecruitmentApplicants } from '@/apis/recruitment';
7 | import { APPLICANT_STATUS_ENG } from '@/constants/recruitment';
8 |
9 | import ApplyInfoItem from './_components/ApplyInfoItem';
10 | import ApprovedCountBox from './_components/ApprovedCountBox';
11 | import ManageApplyItem from './_components/ManageApplyItem';
12 |
13 | function ManageApply() {
14 | const { id: recruitmentId } = useParams<{ id: string }>();
15 | const {
16 | data: {
17 | currentRecruitmentCount,
18 | recruitmentCapacity,
19 | applicants,
20 | approvedCount,
21 | },
22 | } = useSuspenseQuery({
23 | queryKey: ['recruitment', 'manage', 'apply', Number(recruitmentId)],
24 | queryFn: () => getShelterRecruitmentApplicants(Number(recruitmentId)),
25 | select: ({ data: { applicants, recruitmentCapacity } }) => {
26 | return {
27 | applicants,
28 | recruitmentCapacity,
29 | currentRecruitmentCount: applicants.length,
30 | approvedCount: applicants.filter(
31 | ({ applicantStatus }) =>
32 | applicantStatus === APPLICANT_STATUS_ENG.APPROVED,
33 | ).length,
34 | };
35 | },
36 | });
37 |
38 | return (
39 |
40 |
44 | {applicants.map((applicant) => (
45 |
50 | ))}
51 |
52 |
53 | );
54 | }
55 |
56 | export default function ManageApplyPage() {
57 | return (
58 | 봉사 신청 현황 페이지 로딩 중...}>
59 |
60 |
61 | );
62 | }
63 |
--------------------------------------------------------------------------------
/apps/shelter/src/pages/my/_hooks/useFetchShelterProfile.ts:
--------------------------------------------------------------------------------
1 | import { useSuspenseQuery } from '@tanstack/react-query';
2 |
3 | import { getShelterInfoAPI } from '@/apis/shelter';
4 | import { ShelterInfo } from '@/types/apis/shetler';
5 |
6 | type ShelterProfile = {
7 | shelterId: number;
8 | imageUrl: string;
9 | name: string;
10 | email: string;
11 | phoneNumber: string;
12 | sparePhoneNumber: string;
13 | address: string;
14 | addressDetail: string;
15 | isOpenedAddress: boolean;
16 | };
17 |
18 | const createProfile = (response: ShelterInfo): ShelterProfile => {
19 | const {
20 | shelterId,
21 | shelterImageUrl,
22 | shelterName,
23 | shelterEmail,
24 | shelterPhoneNumber,
25 | shelterSparePhoneNumber,
26 | shelterAddress,
27 | shelterAddressDetail,
28 | shelterIsOpenedAddress,
29 | } = response;
30 | return {
31 | shelterId,
32 | imageUrl: shelterImageUrl,
33 | name: shelterName,
34 | email: shelterEmail,
35 | phoneNumber: shelterPhoneNumber,
36 | sparePhoneNumber: shelterSparePhoneNumber,
37 | address: shelterAddress,
38 | addressDetail: shelterAddressDetail,
39 | isOpenedAddress: shelterIsOpenedAddress,
40 | };
41 | };
42 |
43 | const useFetchShelterProfile = () =>
44 | useSuspenseQuery({
45 | queryKey: ['shelterProfile'],
46 | queryFn: async () => {
47 | const response = (await getShelterInfoAPI()).data;
48 | return createProfile(response);
49 | },
50 | });
51 |
52 | export default useFetchShelterProfile;
53 |
--------------------------------------------------------------------------------
/apps/shelter/src/pages/my/reviews/VolunteerProfile.tsx:
--------------------------------------------------------------------------------
1 | import NextIcon from '@anifriends/assets/icon_review_next.svg';
2 | import { InfoSubtext, Label } from '@anifriends/components';
3 | import { Avatar, Box, HStack, Image, Text } from '@chakra-ui/react';
4 |
5 | type VolunteerProfileprops = {
6 | volunteerName: string;
7 | volunteerTempature: number;
8 | volunteerReviewCount: number;
9 | volunteerImageUrl: string;
10 | reviewCreatedAt: string;
11 | onClickNextButton: VoidFunction;
12 | };
13 |
14 | export default function VolunteerProfile({
15 | volunteerName,
16 | volunteerTempature,
17 | volunteerReviewCount,
18 | volunteerImageUrl,
19 | reviewCreatedAt,
20 | onClickNextButton,
21 | }: VolunteerProfileprops) {
22 | return (
23 |
24 |
25 |
26 |
27 |
28 | {volunteerName}
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/apps/shelter/src/pages/my/reviews/hooks/useFetchShelterReviews.tsx:
--------------------------------------------------------------------------------
1 | import { useSuspenseInfiniteQuery } from '@tanstack/react-query';
2 |
3 | import { getShelterReviewList } from '@/apis/shelter';
4 |
5 | export default function useFetchShelterReviews(size: number) {
6 | return useSuspenseInfiniteQuery({
7 | queryKey: ['reviews'],
8 | queryFn: ({ pageParam }) => getShelterReviewList({ page: pageParam, size }),
9 | initialPageParam: 0,
10 | getNextPageParam: ({ data: { pageInfo } }, _, lastPageParam) =>
11 | pageInfo.hasNext ? lastPageParam + 1 : null,
12 | });
13 | }
14 |
--------------------------------------------------------------------------------
/apps/shelter/src/pages/notfound/index.tsx:
--------------------------------------------------------------------------------
1 | export default function NotFoundPage() {
2 | return NotFoundPage
;
3 | }
4 |
--------------------------------------------------------------------------------
/apps/shelter/src/pages/notifications/index.tsx:
--------------------------------------------------------------------------------
1 | import { NotReady } from '@anifriends/components';
2 |
3 | export default function NotificationsPage() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/apps/shelter/src/pages/volunteers/_components/PlusIcon.tsx:
--------------------------------------------------------------------------------
1 | export default function PlusIcon() {
2 | return (
3 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/apps/shelter/src/pages/volunteers/_components/RecruitDateText.tsx:
--------------------------------------------------------------------------------
1 | import { Text } from '@chakra-ui/react';
2 |
3 | type RecruitDateTextProps = {
4 | title: string;
5 | date: string;
6 | time: string;
7 | };
8 |
9 | export default function RecruitDateText({
10 | title,
11 | date,
12 | time,
13 | }: RecruitDateTextProps) {
14 | return {`${title} | ${date} · ${time}`};
15 | }
16 |
--------------------------------------------------------------------------------
/apps/shelter/src/pages/volunteers/_components/RecruitSkeleton.tsx:
--------------------------------------------------------------------------------
1 | import { Skeleton, Stack } from '@chakra-ui/react';
2 |
3 | export default function RecruitSkeleton() {
4 | return (
5 |
12 |
13 |
14 |
15 |
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/apps/shelter/src/pages/volunteers/_components/RecruitSkeletonList.tsx:
--------------------------------------------------------------------------------
1 | import RecruitSkeleton from './RecruitSkeleton';
2 |
3 | export default function RecruitSkeletonList() {
4 | return (
5 | <>
6 |
7 |
8 | >
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/apps/shelter/src/pages/volunteers/_components/VolunteerRecruitItemButton.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from '@chakra-ui/react';
2 | import { ReactNode } from 'react';
3 |
4 | type RecruitItemButtonProps = {
5 | type: 'PRIMARY' | 'SECONDARY';
6 | onClick: VoidFunction;
7 | children: ReactNode;
8 | };
9 |
10 | export default function VolunteerRecruitItemButton({
11 | type,
12 | onClick,
13 | children,
14 | }: RecruitItemButtonProps) {
15 | return (
16 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/apps/shelter/src/pages/volunteers/_utils/recruitment.ts:
--------------------------------------------------------------------------------
1 | import { createFormattedTime, getDDay } from '@anifriends/utils';
2 |
3 | import { Recruitment } from '@/types/apis/recruitment';
4 |
5 | export const createRecruitmentItem = (recruitment: Recruitment) => {
6 | const {
7 | recruitmentId,
8 | recruitmentTitle,
9 | recruitmentStartTime,
10 | recruitmentEndTime,
11 | recruitmentDeadline,
12 | recruitmentIsClosed,
13 | recruitmentApplicantCount,
14 | recruitmentCapacity,
15 | } = recruitment;
16 |
17 | const recruitmentStartDate = new Date(recruitmentStartTime);
18 | const recruitmentEndDate = new Date(recruitmentEndTime);
19 | const recruitmentDeadlineDate = new Date(recruitmentDeadline);
20 |
21 | return {
22 | id: recruitmentId,
23 | title: recruitmentTitle,
24 | isRecruitmentClosed: recruitmentIsClosed,
25 | volunteerDate: createFormattedTime(recruitmentStartDate, 'YYYY.MM.DD'),
26 | volunteerTime: `${createFormattedTime(
27 | recruitmentStartDate,
28 | 'hh:mm',
29 | )}~${createFormattedTime(recruitmentEndDate, 'hh:mm')}`,
30 | deadLineDate: createFormattedTime(recruitmentDeadlineDate, 'YYYY.MM.DD'),
31 | deadLineTime: createFormattedTime(recruitmentDeadlineDate, 'hh:mm'),
32 | volunteerDateDday: getDDay(recruitmentDeadline),
33 | applicantCount: recruitmentApplicantCount,
34 | recruitmentCapacity: recruitmentCapacity,
35 | };
36 | };
37 |
--------------------------------------------------------------------------------
/apps/shelter/src/pages/volunteers/detail/_hooks/useGetVolunteerDetail.ts:
--------------------------------------------------------------------------------
1 | import {
2 | getRecruitmentDetail,
3 | RecruitmentDetailResponse,
4 | } from '@anifriends/apis';
5 | import { useSuspenseQuery } from '@tanstack/react-query';
6 |
7 | export type RecruitmentDetail = {
8 | title: string;
9 | content: string;
10 | applicant: number;
11 | capacity: number;
12 | startTime: string;
13 | endTime: string;
14 | deadline: string;
15 | createdAt: string;
16 | updatedAt: string;
17 | imageUrls: string[];
18 | isClosed: boolean;
19 | };
20 |
21 | const createRecruitmentDetail = (
22 | recruitment: RecruitmentDetailResponse,
23 | ): RecruitmentDetail => {
24 | const {
25 | recruitmentTitle: title,
26 | recruitmentContent: content,
27 | recruitmentApplicantCount: applicant,
28 | recruitmentCapacity: capacity,
29 | recruitmentStartTime: startTime,
30 | recruitmentEndTime: endTime,
31 | recruitmentDeadline: deadline,
32 | recruitmentCreatedAt: createdAt,
33 | recruitmentUpdatedAt: updatedAt,
34 | recruitmentImageUrls: imageUrls,
35 | recruitmentIsClosed: isClosed,
36 | } = recruitment;
37 |
38 | return {
39 | title,
40 | content,
41 | applicant,
42 | capacity,
43 | startTime,
44 | endTime,
45 | deadline,
46 | createdAt,
47 | updatedAt,
48 | imageUrls,
49 | isClosed,
50 | };
51 | };
52 |
53 | const useGetVolunteerDetail = (recruitmentId: number) =>
54 | useSuspenseQuery({
55 | queryKey: ['recruitment', 'detail', recruitmentId],
56 | queryFn: async () => {
57 | const response = (await getRecruitmentDetail(recruitmentId)).data;
58 | return createRecruitmentDetail(response);
59 | },
60 | });
61 |
62 | export default useGetVolunteerDetail;
63 |
--------------------------------------------------------------------------------
/apps/shelter/src/pages/volunteers/search/_components/RecruitmentsSearchFilter.tsx:
--------------------------------------------------------------------------------
1 | import { SearchFilters, SearchFilterSelectData } from '@anifriends/components';
2 | import { PERIOD } from '@anifriends/constants';
3 | import { ChangeEvent } from 'react';
4 |
5 | import {
6 | RECRUITMENT_STATUS,
7 | SEARCH_TYPE,
8 | } from '@/pages/volunteers/search/_constants/filter';
9 | import { SearchFilter } from '@/pages/volunteers/search/_types/filter';
10 |
11 | type RecruitmentsSearchFilterProps = {
12 | searchFilter: Partial;
13 | onChangeFilter: (event: ChangeEvent) => void;
14 | };
15 |
16 | export default function RecruitmentsSearchFilter({
17 | searchFilter,
18 | onChangeFilter,
19 | }: RecruitmentsSearchFilterProps) {
20 | const searchFilters: SearchFilterSelectData[] = [
21 | {
22 | selectOption: PERIOD,
23 | name: 'period',
24 | placeholder: '봉사일',
25 | value: searchFilter.period,
26 | },
27 | {
28 | selectOption: RECRUITMENT_STATUS,
29 | name: 'recruitmentStatus',
30 | placeholder: '모집',
31 | value: searchFilter.recruitmentStatus,
32 | },
33 | {
34 | selectOption: SEARCH_TYPE,
35 | name: 'searchType',
36 | placeholder: '전체',
37 | value: searchFilter.searchType,
38 | },
39 | ];
40 |
41 | return (
42 |
46 | );
47 | }
48 |
--------------------------------------------------------------------------------
/apps/shelter/src/pages/volunteers/search/_constants/filter.ts:
--------------------------------------------------------------------------------
1 | export const RECRUITMENT_STATUS = {
2 | IS_OPENED: '모집 중',
3 | IS_CLOSED: '모집 완료',
4 | } as const;
5 |
6 | export const SEARCH_TYPE = {
7 | IS_TITLE: '제목 포함',
8 | IS_CONTENT: '내용 포함',
9 | } as const;
10 |
--------------------------------------------------------------------------------
/apps/shelter/src/pages/volunteers/search/_hooks/useRecruitmentSearch.ts:
--------------------------------------------------------------------------------
1 | import { useSearchFilter, useSearchKeyword } from '@anifriends/hooks';
2 | import { ChangeEvent } from 'react';
3 |
4 | import { SearchFilter } from '@/pages/volunteers/search/_types/filter';
5 |
6 | export const useRecruitmentSearch = () => {
7 | const [searchFilter, setSearchFilter] = useSearchFilter();
8 |
9 | const setKeywordFilter = (keyword: string) => setSearchFilter({ keyword });
10 |
11 | useSearchKeyword(setKeywordFilter);
12 |
13 | const handleChangeSearchFilter = (event: ChangeEvent) => {
14 | const { name, value } = event.target;
15 |
16 | setSearchFilter({ [name]: value });
17 | };
18 |
19 | return {
20 | searchFilter,
21 | isKeywordSearched: Boolean(searchFilter.keyword),
22 | handleChangeSearchFilter,
23 | };
24 | };
25 |
--------------------------------------------------------------------------------
/apps/shelter/src/pages/volunteers/search/_types/filter.ts:
--------------------------------------------------------------------------------
1 | import { Period } from '@anifriends/types';
2 |
3 | import {
4 | RECRUITMENT_STATUS,
5 | SEARCH_TYPE,
6 | } from '@/pages/volunteers/search/_constants/filter';
7 |
8 | export type RecruitmentStatus = keyof typeof RECRUITMENT_STATUS;
9 | export type SearchType = keyof typeof SEARCH_TYPE;
10 |
11 | export type SearchFilter = {
12 | keyword: string;
13 | period: Period;
14 | recruitmentStatus: RecruitmentStatus;
15 | searchType: SearchType;
16 | };
17 |
--------------------------------------------------------------------------------
/apps/shelter/src/react-query.d.ts:
--------------------------------------------------------------------------------
1 | import '@tanstack/react-query';
2 |
3 | import { AxiosError } from 'axios';
4 | import { ErrorResponseData } from 'shared/types/apis/error';
5 |
6 | declare module '@tanstack/react-query' {
7 | interface Register {
8 | defaultError: AxiosError;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/apps/shelter/src/types/apis/auth.ts:
--------------------------------------------------------------------------------
1 | import { SigninRequestData } from '@anifriends/types';
2 |
3 | export type SignupRequestData = SigninRequestData & {
4 | name: string;
5 | address: string;
6 | addressDetail: string;
7 | phoneNumber: string;
8 | sparePhoneNumber: string;
9 | isOpenedAddress: boolean;
10 | };
11 |
--------------------------------------------------------------------------------
/apps/shelter/src/types/apis/shetler.ts:
--------------------------------------------------------------------------------
1 | export type ShelterInfo = {
2 | shelterId: number;
3 | shelterEmail: string;
4 | shelterName: string;
5 | shelterImageUrl: string;
6 | shelterAddress: string;
7 | shelterAddressDetail: string;
8 | shelterPhoneNumber: string;
9 | shelterSparePhoneNumber: string;
10 | shelterIsOpenedAddress: boolean;
11 | };
12 |
13 | export type UpdateShelterInfo = {
14 | imageUrl?: string;
15 | name: string;
16 | address: string;
17 | addressDetail: string;
18 | phoneNumber: string;
19 | sparePhoneNumber: string;
20 | isOpenedAddress: boolean;
21 | };
22 |
--------------------------------------------------------------------------------
/apps/shelter/src/types/apis/volunteers.ts:
--------------------------------------------------------------------------------
1 | type PageInfo = {
2 | totalElements: number;
3 | hasNext: boolean;
4 | };
5 |
6 | type Recruitment = {
7 | recruitmentId: number;
8 | recruitmentTitle: string;
9 | recruitmentStartTime: string;
10 | shelterName: string;
11 | };
12 |
13 | type Review = {
14 | reviewId: number;
15 | shelterName: string;
16 | reviewCreatedAt: string;
17 | reviewContent: string;
18 | reviewImageUrls: string[];
19 | };
20 |
21 | export type VolunteerProfileResponseData = {
22 | volunteerEmail: string;
23 | volunteerName: string;
24 | volunteerTemperature: number;
25 | volunteerImageUrl: string;
26 | volunteerPhoneNumber: string;
27 | };
28 |
29 | export type VoluteerRecruitmentsOnVolunteerResponseData = {
30 | pageInfo: PageInfo;
31 | recruitments: Recruitment[];
32 | };
33 |
34 | export type VolunteerCompletedsRequestParams = {
35 | page: number;
36 | size: number;
37 | };
38 |
39 | export type VolunteerReviewsOnVolunteerResponseData = {
40 | pageInfo: PageInfo;
41 | reviews: Review[];
42 | };
43 |
--------------------------------------------------------------------------------
/apps/shelter/src/types/recruitment.ts:
--------------------------------------------------------------------------------
1 | import {
2 | APPLICANT_STATUS_ENG,
3 | APPLICANT_STATUS_KOR,
4 | } from '@/constants/recruitment';
5 |
6 | export type applicantStatusEng =
7 | (typeof APPLICANT_STATUS_ENG)[keyof typeof APPLICANT_STATUS_ENG];
8 |
9 | export type applicantStatusKor =
10 | (typeof APPLICANT_STATUS_KOR)[keyof typeof APPLICANT_STATUS_KOR];
11 |
--------------------------------------------------------------------------------
/apps/shelter/src/utils/test/render.tsx:
--------------------------------------------------------------------------------
1 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
2 | import { render } from '@testing-library/react';
3 | import userEvent from '@testing-library/user-event';
4 | import React from 'react';
5 | import { MemoryRouter, RouterProps } from 'react-router-dom';
6 |
7 | const queryClient = new QueryClient({
8 | defaultOptions: {
9 | queries: {
10 | // ✅ turns retries off
11 | retry: false,
12 | },
13 | },
14 | });
15 |
16 | // eslint-disable-next-line react-refresh/only-export-components
17 | export default async (
18 | component: React.ReactNode,
19 | options: { routerProps?: RouterProps },
20 | ) => {
21 | const { routerProps } = options;
22 | const user = userEvent.setup();
23 |
24 | return {
25 | user,
26 | ...render(
27 |
28 | {component}
29 | ,
30 | ),
31 | };
32 | };
33 |
--------------------------------------------------------------------------------
/apps/shelter/src/utils/test/setupTests.ts:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom';
2 |
3 | afterEach(() => {
4 | vi.clearAllMocks();
5 | });
6 |
7 | afterAll(() => {
8 | vi.resetAllMocks();
9 | });
10 |
11 | // https://github.com/vitest-dev/vitest/issues/821
12 | Object.defineProperty(window, 'matchMedia', {
13 | writable: true,
14 | value: vi.fn().mockImplementation((query) => ({
15 | matches: false,
16 | media: query,
17 | onchange: null,
18 | addListener: vi.fn(), // deprecated
19 | removeListener: vi.fn(), // deprecated
20 | addEventListener: vi.fn(),
21 | removeEventListener: vi.fn(),
22 | dispatchEvent: vi.fn(),
23 | })),
24 | });
25 |
--------------------------------------------------------------------------------
/apps/shelter/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/apps/shelter/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@anifriends/tsconfig/vite.json",
3 | "include": ["src"],
4 | "compilerOptions": {
5 | "baseUrl": ".",
6 | "paths": {
7 | "@/*": ["src/*"]
8 | },
9 | "types": ["vitest/globals"]
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/apps/shelter/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "rewrites": [{ "source": "/(.*)", "destination": "/" }]
3 | }
4 |
--------------------------------------------------------------------------------
/apps/shelter/vite.config.ts:
--------------------------------------------------------------------------------
1 | import react from '@vitejs/plugin-react-swc';
2 | import { defineConfig } from 'vite';
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig(() => ({
6 | plugins: [react()],
7 | resolve: {
8 | alias: [{ find: '@', replacement: '/src' }],
9 | },
10 | test: {
11 | globals: true,
12 | environment: 'jsdom',
13 | setupFiles: './src/utils/test/setupTests.ts',
14 | },
15 | }));
16 |
--------------------------------------------------------------------------------
/apps/volunteer/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | extends: ['anifriends'],
4 | overrides: [
5 | {
6 | files: ['__mocks__/**.ts', 'src/**/*.spec.ts', 'src/**/*.spec.tsx'],
7 | plugins: ['vitest'],
8 | extends: ['plugin:vitest/recommended'],
9 | rules: {
10 | 'vitest/expect-expect': 'off',
11 | },
12 | globals: {
13 | globalThis: true,
14 | describe: true,
15 | it: true,
16 | expect: true,
17 | beforeEach: true,
18 | afterEach: true,
19 | beforeAll: true,
20 | afterAll: true,
21 | vi: true,
22 | },
23 | },
24 | ],
25 | };
26 |
--------------------------------------------------------------------------------
/apps/volunteer/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/apps/volunteer/README.md:
--------------------------------------------------------------------------------
1 | # React + TypeScript + Vite
2 |
3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4 |
5 | Currently, two official plugins are available:
6 |
7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9 |
10 | ## Expanding the ESLint configuration
11 |
12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
13 |
14 | - Configure the top-level `parserOptions` property like this:
15 |
16 | ```js
17 | parserOptions: {
18 | ecmaVersion: 'latest',
19 | sourceType: 'module',
20 | project: ['./tsconfig.json', './tsconfig.node.json'],
21 | tsconfigRootDir: __dirname,
22 | },
23 | ```
24 |
25 | - Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
26 | - Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
27 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list
28 |
--------------------------------------------------------------------------------
/apps/volunteer/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
13 |
14 | Anifriends
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/apps/volunteer/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anifriends/frontend/8b346b0db582ab9f5894e83c133762b8bdbeae9e/apps/volunteer/public/favicon.ico
--------------------------------------------------------------------------------
/apps/volunteer/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/volunteer/src/App.tsx:
--------------------------------------------------------------------------------
1 | import Fonts from '@anifriends/fonts';
2 | import theme from '@anifriends/theme';
3 | import { ChakraProvider } from '@chakra-ui/react';
4 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
5 | import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
6 | import { RouterProvider } from 'react-router-dom';
7 |
8 | import { router } from '@/routes';
9 |
10 | const queryClient = new QueryClient({
11 | defaultOptions: {
12 | queries: {
13 | staleTime: 50 * 1000,
14 | retry: false,
15 | },
16 | },
17 | });
18 |
19 | export default function App() {
20 | return (
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/apps/volunteer/src/apis/animal.ts:
--------------------------------------------------------------------------------
1 | import { axiosInstance } from '@anifriends/apis';
2 |
3 | type PageInfo = {
4 | totalElements: number;
5 | hasNext: boolean;
6 | };
7 |
8 | type Animal = {
9 | animalId: number;
10 | animalName: string;
11 | shelterName: string;
12 | shelterAddress: string;
13 | animalImage: string;
14 | };
15 |
16 | export const searchVolunteerAnimals = () =>
17 | axiosInstance.get<
18 | {
19 | pageInfo: PageInfo;
20 | animals: Animal[];
21 | },
22 | {
23 | type: string;
24 | gender: string;
25 | isNeutered: boolean;
26 | active: string;
27 | size: string;
28 | age: string;
29 | pageNumber: number;
30 | pageSize: number;
31 | }
32 | >('/volunteers/animals');
33 |
34 | type Shelter = {
35 | shelterId: number;
36 | name: string;
37 | imageUrl: string;
38 | email: string;
39 | address: string;
40 | };
41 |
42 | export const getVolunteerAnimalDetail = (animalId: number) => {
43 | return axiosInstance.get<{
44 | name: string;
45 | birthDate: string;
46 | breed: string;
47 | gender: string;
48 | isNeutered: boolean;
49 | active: string;
50 | weight: number;
51 | information: string;
52 | animalImageUrls: string[];
53 | shelter: Shelter;
54 | }>(`/volunteers/animals/${animalId}`);
55 | };
56 |
--------------------------------------------------------------------------------
/apps/volunteer/src/apis/auth.ts:
--------------------------------------------------------------------------------
1 | import { axiosInstance } from '@anifriends/apis';
2 | import type {
3 | ChangePasswordRequestData,
4 | CheckDuplicatedEmailRequestData,
5 | CheckDuplicatedEmailResponseData,
6 | SigninRequestData,
7 | SigninResponseData,
8 | } from '@anifriends/types';
9 |
10 | import { SignupRequestData } from '@/types/apis/auth';
11 |
12 | export const signinVolunteer = (data: SigninRequestData) =>
13 | axiosInstance.post(
14 | '/auth/volunteers/login',
15 | data,
16 | );
17 |
18 | export const signupVolunteer = (data: SignupRequestData) =>
19 | axiosInstance.post('/volunteers', data);
20 |
21 | export const checkDuplicatedVolunteerEmail = (
22 | data: CheckDuplicatedEmailRequestData,
23 | ) =>
24 | axiosInstance.post<
25 | CheckDuplicatedEmailResponseData,
26 | CheckDuplicatedEmailRequestData
27 | >('/volunteers/email', data);
28 |
29 | export const changeVolunteerPassword = (data: ChangePasswordRequestData) =>
30 | axiosInstance.patch(
31 | '/volunteers/me/passwords',
32 | data,
33 | );
34 |
--------------------------------------------------------------------------------
/apps/volunteer/src/apis/recruitment.ts:
--------------------------------------------------------------------------------
1 | import { axiosInstance } from '@anifriends/apis';
2 |
3 | import {
4 | RecruitementsResponse,
5 | RecruitmentsRequest,
6 | } from '@/types/apis/recruitment';
7 |
8 | export const applyRecruitments = (recruitmentId: number) =>
9 | axiosInstance.post(`/volunteers/recruitments/${recruitmentId}/apply`);
10 |
11 | export const getRecruitments = (request: Partial) =>
12 | axiosInstance.get(
13 | '/recruitments',
14 | { params: request },
15 | );
16 |
17 | type IsAppliedRecruitmentResponse = {
18 | isAppliedRecruitment: boolean;
19 | };
20 |
21 | export const getIsAppliedRecruitment = (recruitmentId: number) =>
22 | axiosInstance.get(
23 | `/volunteers/recruitments/${recruitmentId}/apply`,
24 | );
25 |
--------------------------------------------------------------------------------
/apps/volunteer/src/apis/review.ts:
--------------------------------------------------------------------------------
1 | import { axiosInstance } from '@anifriends/apis';
2 |
3 | import {
4 | ReviewCreateRequest,
5 | ReviewDetailResponse,
6 | ReviewOnShelterResponse,
7 | ReviewsOnShelterRequest,
8 | ReviewUpdateRequest,
9 | } from '@/types/apis/review';
10 |
11 | export const getVolunteerReviewDetail = (reviewId: number) =>
12 | axiosInstance.get(`/volunteers/reviews/${reviewId}`);
13 |
14 | export const createVolunteerReview = (reqeust: ReviewCreateRequest) =>
15 | axiosInstance.post(
16 | '/volunteers/reviews',
17 | reqeust,
18 | );
19 |
20 | export const updateVolunteerReview = (
21 | reviewId: number,
22 | reqeust: ReviewUpdateRequest,
23 | ) =>
24 | axiosInstance.patch(
25 | `/volunteers/reviews/${reviewId}`,
26 | reqeust,
27 | );
28 |
29 | export const deleteVolunteerReview = (reviewId: number) =>
30 | axiosInstance.delete(`/volunteers/reviews/${reviewId}`);
31 |
32 | export const getVolunteerReviewsOnShelter = (
33 | shelterId: number,
34 | request: ReviewsOnShelterRequest,
35 | ) =>
36 | axiosInstance.get(
37 | `/shelters/${shelterId}/reviews`,
38 | { params: { ...request } },
39 | );
40 |
--------------------------------------------------------------------------------
/apps/volunteer/src/apis/shelter.ts:
--------------------------------------------------------------------------------
1 | import { axiosInstance } from '@anifriends/apis';
2 |
3 | export type SimpleShelterProfile = {
4 | shelterName: string;
5 | shelterImageUrl: string;
6 | shelterAddress: string;
7 | shelterEmail: string;
8 | };
9 |
10 | export type ShelterProfile = {
11 | shelterId: number;
12 | shelterAddressDetail: string;
13 | shelterPhoneNumber: string;
14 | shelterSparePhoneNumber: string;
15 | } & SimpleShelterProfile;
16 |
17 | export const getSimpleShelterProfile = (shelterId: number) =>
18 | axiosInstance.get(
19 | `/shelters/${shelterId}/profile/simple`,
20 | );
21 |
22 | export const getShelterProfileDetail = (shelterId: number) =>
23 | axiosInstance.get(`/shelters/${shelterId}/profile`);
24 |
25 | export type RecruitmentOfShleter = {
26 | recruitmentId: number;
27 | recruitmentTitle: string;
28 | recruitmentStartTime: string;
29 | recruitmentDeadline: string;
30 | recruitmentApplicantCount: number;
31 | recruitmentCapacity: number;
32 | };
33 |
34 | type PageInfo = {
35 | totalElements: number;
36 | hasNext: boolean;
37 | };
38 |
39 | type RecruitmentsOfShelterResponse = {
40 | pageInfo: PageInfo;
41 | recruitments: RecruitmentOfShleter[];
42 | };
43 |
44 | type RecruitmentOfShelterRequest = {
45 | page: number;
46 | size: number;
47 | };
48 |
49 | export const getRecruitementsOfShelter = (
50 | shelterId: number,
51 | page: number,
52 | size: number,
53 | ) =>
54 | axiosInstance.get(
55 | `/shelters/${shelterId}/recruitments`,
56 | {
57 | params: {
58 | page,
59 | size,
60 | },
61 | },
62 | );
63 |
--------------------------------------------------------------------------------
/apps/volunteer/src/apis/volunteer.ts:
--------------------------------------------------------------------------------
1 | import { axiosInstance } from '@anifriends/apis';
2 |
3 | import { PageInfo } from '@/types/apis/recruitment';
4 | import {
5 | PagenationRequestParams,
6 | VolunteerApplicantsResponseData,
7 | } from '@/types/apis/volunteer';
8 |
9 | export type MyInfoResponse = {
10 | volunteerId: number;
11 | volunteerEmail: string;
12 | volunteerName: string;
13 | volunteerBirthDate: string;
14 | volunteerPhoneNumber: string;
15 | volunteerTemperature: number;
16 | completedVolunteerCount: number;
17 | volunteerImageUrl: string;
18 | volunteerGender: 'FEMALE' | 'MALE';
19 | };
20 |
21 | export const getMyVolunteerInfo = () =>
22 | axiosInstance.get('/volunteers/me');
23 |
24 | export type UpdateUserInfoParams = {
25 | name: string;
26 | gender: 'FEMALE' | 'MALE';
27 | birthDate: string;
28 | phoneNumber: string;
29 | imageUrl?: string;
30 | };
31 |
32 | export const updateVolunteerUserInfo = (
33 | updateUserInfoParams: UpdateUserInfoParams,
34 | ) =>
35 | axiosInstance.patch(
36 | '/volunteers/me',
37 | updateUserInfoParams,
38 | );
39 |
40 | //봉사자가 신청한 봉사 리스트 조회
41 | export const getVolunteerApplicants = (params: PagenationRequestParams) =>
42 | axiosInstance.get('/volunteers/applicants', {
43 | params,
44 | });
45 |
46 | type Pagination = {
47 | page: number;
48 | size: number;
49 | };
50 |
51 | type MyReview = {
52 | reviewId: number;
53 | shelterId: number;
54 | shelterName: string;
55 | reviewCreatedAt: string;
56 | reviewContent: string;
57 | reviewImageUrls: string[];
58 | };
59 |
60 | export type MyReviewsResponse = {
61 | pageInfo: PageInfo;
62 | reviews: MyReview[];
63 | };
64 |
65 | export const getMyReviewsAPI = (page: number, size: number) =>
66 | axiosInstance.get('/volunteers/me/reviews', {
67 | params: {
68 | page,
69 | size,
70 | },
71 | });
72 |
--------------------------------------------------------------------------------
/apps/volunteer/src/constants/applicantStatus.ts:
--------------------------------------------------------------------------------
1 | export const APPLICANT_STATUS = {
2 | PENDING: { ENG: 'PENDING', KOR: '대기중', COLOR: 'YELLOW' },
3 | REFUSED: { ENG: 'REFUSED', KOR: '거절됨', COLOR: 'RED' },
4 | APPROVED: { ENG: 'APPROVED', KOR: '승인완료', COLOR: 'ORANGE' },
5 | ATTENDANCE: { ENG: 'ATTENDANCE', KOR: '출석완료', COLOR: 'GREEN' },
6 | NOSHOW: { ENG: 'NOSHOW', KOR: '불참', COLOR: 'RED' },
7 | } as const;
8 |
--------------------------------------------------------------------------------
/apps/volunteer/src/constants/path.ts:
--------------------------------------------------------------------------------
1 | const PATH = {
2 | VOLUNTEERS: {
3 | INDEX: 'volunteers',
4 | DETAIL: ':id',
5 | SEARCH: 'search',
6 | },
7 | ANIMALS: {
8 | INDEX: 'animals',
9 | DETAIL: ':id',
10 | },
11 | CHATTINGS: {
12 | INDEX: 'chattings',
13 | ROOM: ':id',
14 | },
15 | MYPAGE: {
16 | INDEX: 'mypage',
17 | REVIEWS: 'mypage/reviews',
18 | },
19 | SETTINGS: {
20 | INDEX: 'settings',
21 | ACCOUNT: 'account',
22 | PASSWORD: 'password',
23 | },
24 | SHELTERS: {
25 | INDEX: 'shelters',
26 | PROFILE: 'profile/:id',
27 | REVIEWS_WRITE: ':shelterId/reviews/applicants/:applicantId/write',
28 | REVIEWS_UPDATE: ':shelterId/reviews/write/:reviewId',
29 | },
30 | NOTIFICATIONS: 'notifications',
31 | SIGNUP: 'signup',
32 | SIGNIN: 'signin',
33 | };
34 |
35 | export default PATH;
36 |
--------------------------------------------------------------------------------
/apps/volunteer/src/main.tsx:
--------------------------------------------------------------------------------
1 | import ReactDOM from 'react-dom/client';
2 |
3 | import App from './App.tsx';
4 |
5 | async function deferRender() {
6 | if (import.meta.env.MODE !== 'development') {
7 | return;
8 | }
9 |
10 | const { worker } = await import('./mocks/browser');
11 |
12 | // `worker.start()` returns a Promise that resolves
13 | // once the Service Worker is up and ready to intercept requests.
14 | return worker.start();
15 | }
16 |
17 | deferRender().then(() => {
18 | ReactDOM.createRoot(document.getElementById('root')!).render();
19 | });
20 |
--------------------------------------------------------------------------------
/apps/volunteer/src/mocks/browser.ts:
--------------------------------------------------------------------------------
1 | import { setupWorker } from 'msw/browser';
2 |
3 | import { handlers as authHandlers } from './handlers/auth';
4 | import { handlers as imageHandlers } from './handlers/image';
5 | import { handlers as recruitmentHandler } from './handlers/recruitment';
6 | import { handlers as reviewHandler } from './handlers/review';
7 | import { handlers as shelterHandler } from './handlers/shelter';
8 | import { handlers as volunteerHandlers } from './handlers/volunteer';
9 |
10 | export const worker = setupWorker(
11 | ...authHandlers,
12 | ...imageHandlers,
13 | ...recruitmentHandler,
14 | ...reviewHandler,
15 | ...shelterHandler,
16 | ...volunteerHandlers,
17 | );
18 |
--------------------------------------------------------------------------------
/apps/volunteer/src/mocks/handlers/image.ts:
--------------------------------------------------------------------------------
1 | import { delay, http, HttpResponse } from 'msw';
2 |
3 | export const handlers = [
4 | http.post('/images', async () => {
5 | await delay(200);
6 |
7 | return HttpResponse.json(
8 | {
9 | imageUrls: ['https://source.unsplash.com/random'],
10 | },
11 | { status: 200 },
12 | );
13 | }),
14 | ];
15 |
--------------------------------------------------------------------------------
/apps/volunteer/src/mocks/handlers/review.ts:
--------------------------------------------------------------------------------
1 | import { delay, http, HttpResponse } from 'msw';
2 |
3 | export const handlers = [
4 | http.post('/volunteers/reviews', async () => {
5 | await delay(200);
6 | return HttpResponse.json(null, { status: 200 });
7 | }),
8 | http.patch('/volunteers/reviews/:id', async () => {
9 | await delay(200);
10 | return HttpResponse.json(null, { status: 200 });
11 | }),
12 | http.get('/volunteers/reviews/:id', async () => {
13 | await delay(200);
14 | return HttpResponse.json(
15 | {
16 | reviewId: 1,
17 | reviewContent: `정말 강아지들이 귀여워서 봉사할 맛이 났어요!\n유기견 입양 한다면 꼭 여기서 입양할 거에요!`,
18 | reviewImageUrls: [
19 | 'https://source.unsplash.com/random/1',
20 | 'https://source.unsplash.com/random/2',
21 | 'https://source.unsplash.com/random/3',
22 | ],
23 | },
24 | { status: 200 },
25 | );
26 | }),
27 | ];
28 |
--------------------------------------------------------------------------------
/apps/volunteer/src/pages/animals/detail/index.tsx:
--------------------------------------------------------------------------------
1 | export default function AnimalsDetailPage() {
2 | return AnimalsDetailPage
;
3 | }
4 |
--------------------------------------------------------------------------------
/apps/volunteer/src/pages/animals/index.tsx:
--------------------------------------------------------------------------------
1 | import { NotReady } from '@anifriends/components';
2 |
3 | export default function AnimalsPage() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/apps/volunteer/src/pages/chattings/index.tsx:
--------------------------------------------------------------------------------
1 | import { NotReady } from '@anifriends/components';
2 |
3 | export default function ChattingsPage() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/apps/volunteer/src/pages/chattings/room/index.tsx:
--------------------------------------------------------------------------------
1 | export default function ChattingsRoomPage() {
2 | return ChattingsRoomPage
;
3 | }
4 |
--------------------------------------------------------------------------------
/apps/volunteer/src/pages/my/_hooks/useFetchMyVolunteer.ts:
--------------------------------------------------------------------------------
1 | import { useSuspenseQuery } from '@tanstack/react-query';
2 |
3 | import { getMyVolunteerInfo } from '@/apis/volunteer';
4 |
5 | const useFetchMyVolunteer = () =>
6 | useSuspenseQuery({
7 | queryKey: ['myVolunteer'],
8 | queryFn: async () => (await getMyVolunteerInfo()).data,
9 | });
10 |
11 | export default useFetchMyVolunteer;
12 |
--------------------------------------------------------------------------------
/apps/volunteer/src/pages/my/index.tsx:
--------------------------------------------------------------------------------
1 | import { Label, ProfileInfo, Tabs } from '@anifriends/components';
2 | import { Box, Divider, Highlight } from '@chakra-ui/react';
3 | import { Suspense } from 'react';
4 |
5 | import ApplyRecruitments from './_components/ApplyRecruitments';
6 | import MyReviewsTab from './_components/MyReviews';
7 | import useFetchMyVolunteer from './_hooks/useFetchMyVolunteer';
8 |
9 | function My() {
10 | const { data } = useFetchMyVolunteer();
11 |
12 | return (
13 |
14 |
19 |
20 |
21 |
22 |
32 |
36 | {`${data.volunteerName} 님께서는 봉사를 ${data.completedVolunteerCount}회 완료했어요!`}
37 |
38 |
39 | 신청한 봉사 목록 로딩 중...}>
44 |
45 | ,
46 | ],
47 | [
48 | '작성한 봉사 후기',
49 | 작성한 봉사 후기 로딩 중...}>
50 |
51 | ,
52 | ],
53 | ]}
54 | />
55 |
56 | );
57 | }
58 |
59 | export default function MyPage() {
60 | return (
61 | 봉사자 마이 페이지 로딩 중...}>
62 |
63 |
64 | );
65 | }
66 |
--------------------------------------------------------------------------------
/apps/volunteer/src/pages/notfound/index.tsx:
--------------------------------------------------------------------------------
1 | export default function NotFoundPage() {
2 | return NotFoundPage
;
3 | }
4 |
--------------------------------------------------------------------------------
/apps/volunteer/src/pages/notifications/index.tsx:
--------------------------------------------------------------------------------
1 | import { NotReady } from '@anifriends/components';
2 |
3 | export default function NotificationsPage() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/apps/volunteer/src/pages/settings/index.tsx:
--------------------------------------------------------------------------------
1 | import { SettingGroup } from '@anifriends/components';
2 | import { APP_TYPE } from '@anifriends/constants';
3 | import { useAuthStore } from '@anifriends/store';
4 | import { removeItemFromStorage } from '@anifriends/utils';
5 | import { Box, useToast, VStack } from '@chakra-ui/react';
6 | import { useNavigate } from 'react-router-dom';
7 |
8 | export default function SettingsPage() {
9 | const navigate = useNavigate();
10 | const goSettingsAccount = () => navigate('/settings/account');
11 | const goSettingsPassword = () => navigate('/settings/password');
12 | const { setUser } = useAuthStore();
13 | const toast = useToast();
14 | const logout = () => {
15 | setUser(null);
16 | removeItemFromStorage(APP_TYPE.VOLUNTEER_APP);
17 | navigate('/volunteers');
18 | toast({
19 | position: 'top',
20 | description: '로그아웃 되었습니다.',
21 | status: 'success',
22 | duration: 1500,
23 | });
24 | };
25 |
26 | return (
27 |
28 |
29 |
36 |
40 |
41 |
42 | );
43 | }
44 |
--------------------------------------------------------------------------------
/apps/volunteer/src/pages/shelters/profile/index.tsx:
--------------------------------------------------------------------------------
1 | import { InfoTextList, ProfileInfo, Tabs } from '@anifriends/components';
2 | import { Box, Divider } from '@chakra-ui/react';
3 | import { useSuspenseQuery } from '@tanstack/react-query';
4 | import { Suspense } from 'react';
5 | import { useParams } from 'react-router-dom';
6 |
7 | import { getShelterProfileDetail } from '@/apis/shelter';
8 |
9 | import ShelterRecruitments from './_components/ShelterRecruitments';
10 | import ShelterReviewsTab from './_components/ShelterReviews';
11 |
12 | function SheltersProfile() {
13 | const { id } = useParams<{ id: string }>();
14 | const shelterId = Number(id);
15 |
16 | const { data } = useSuspenseQuery({
17 | queryKey: ['shelter', 'profile', shelterId],
18 | queryFn: async () => (await getShelterProfileDetail(shelterId)).data,
19 | });
20 |
21 | return (
22 |
23 |
28 |
29 |
42 | 봉사 후기 로징 중...} key={1}>
47 |
48 | ,
49 | ],
50 | [
51 | '봉사모집글',
52 | 봉사 모집글 로딩 중...} key={2}>
53 |
54 | ,
55 | ],
56 | ]}
57 | />
58 |
59 | );
60 | }
61 |
62 | export default function SheltersProfilePage() {
63 | return (
64 | 보호소 프로필 페이지 로딩 중...}>
65 |
66 |
67 | );
68 | }
69 |
--------------------------------------------------------------------------------
/apps/volunteer/src/pages/shelters/reviews/_components/ReviewSubmitButton.tsx:
--------------------------------------------------------------------------------
1 | import { Button, ButtonProps, VStack } from '@chakra-ui/react';
2 |
3 | type ReviewSubmitButton = {
4 | formId: string;
5 | buttonText: string;
6 | } & ButtonProps;
7 |
8 | export default function ReviewSubmitButton({
9 | formId,
10 | buttonText,
11 | isLoading,
12 | }: ReviewSubmitButton) {
13 | return (
14 |
27 |
39 |
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/apps/volunteer/src/pages/shelters/reviews/_constants/reviews.ts:
--------------------------------------------------------------------------------
1 | export const UPLOAD_LIMIT = 5;
2 | export const MAX_REVIEW_CONTENTS_LENGTH = 500;
3 | export const FORM_ID = 'shelterReview';
4 |
--------------------------------------------------------------------------------
/apps/volunteer/src/pages/shelters/reviews/_schema/reviewSchema.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod';
2 |
3 | import { MAX_REVIEW_CONTENTS_LENGTH } from '@/pages/shelters/reviews/_constants/reviews';
4 |
5 | export const reviewSchema = z.object({
6 | content: z
7 | .string()
8 | .refine(
9 | (val) => val?.length && val.length < MAX_REVIEW_CONTENTS_LENGTH,
10 | '에러입니다',
11 | ),
12 | });
13 |
14 | export type ReviewSchema = z.infer;
15 |
--------------------------------------------------------------------------------
/apps/volunteer/src/pages/volunteers/_components/RecruitSkeleton.tsx:
--------------------------------------------------------------------------------
1 | import { Flex, Skeleton, Stack } from '@chakra-ui/react';
2 |
3 | export default function RecruitSkeleton() {
4 | return (
5 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/apps/volunteer/src/pages/volunteers/_components/RecruitSkeletonList.tsx:
--------------------------------------------------------------------------------
1 | import RecruitSkeleton from './RecruitSkeleton';
2 |
3 | export default function RecruitSkeletonList() {
4 | return (
5 | <>
6 |
7 |
8 | >
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/apps/volunteer/src/pages/volunteers/_utils/recruitment.ts:
--------------------------------------------------------------------------------
1 | import { createFormattedTime, getDDay } from '@anifriends/utils';
2 |
3 | import { Recruitment } from '@/types/apis/recruitment';
4 |
5 | export const createRecruitmentItem = (recruitment: Recruitment) => {
6 | const {
7 | recruitmentId,
8 | recruitmentTitle,
9 | shelterName,
10 | shelterImageUrl,
11 | recruitmentApplicantCount,
12 | recruitmentCapacity,
13 | recruitmentStartTime,
14 | recruitmentDeadline,
15 | recruitmentIsClosed,
16 | } = recruitment;
17 |
18 | return {
19 | id: recruitmentId,
20 | title: recruitmentTitle,
21 | shelterName: shelterName,
22 | shelterProfileImage: shelterImageUrl,
23 | isRecruitmentClosed: recruitmentIsClosed,
24 | volunteerDate: createFormattedTime(
25 | new Date(recruitmentStartTime),
26 | 'YY.MM.DD',
27 | ),
28 | volunteerDateDday: getDDay(recruitmentDeadline),
29 | applicantCount: recruitmentApplicantCount,
30 | recruitmentCapacity: recruitmentCapacity,
31 | };
32 | };
33 |
--------------------------------------------------------------------------------
/apps/volunteer/src/pages/volunteers/detail/_hooks/useFetchRecruitmentDetail.ts:
--------------------------------------------------------------------------------
1 | import { getRecruitmentDetail } from '@anifriends/apis';
2 | import { User } from '@anifriends/store';
3 | import { useSuspenseQueries } from '@tanstack/react-query';
4 |
5 | import { getIsAppliedRecruitment } from '@/apis/recruitment';
6 |
7 | const useFetchRecruitmentDetail = (
8 | recruitmentId: number,
9 | user: User | null,
10 | ) => {
11 | return useSuspenseQueries({
12 | queries: [
13 | {
14 | queryKey: ['recruitment', recruitmentId],
15 | queryFn: async () => (await getRecruitmentDetail(recruitmentId)).data,
16 | },
17 | {
18 | queryKey: ['recruitment', recruitmentId, 'isApplied'],
19 | queryFn: async () => {
20 | if (!user) {
21 | return { isAppliedRecruitment: false };
22 | }
23 | return (await getIsAppliedRecruitment(recruitmentId)).data;
24 | },
25 | },
26 | ],
27 | });
28 | };
29 | export default useFetchRecruitmentDetail;
30 |
--------------------------------------------------------------------------------
/apps/volunteer/src/pages/volunteers/detail/_hooks/useFetchSimpleShelterInfo.ts:
--------------------------------------------------------------------------------
1 | import { useSuspenseQuery } from '@tanstack/react-query';
2 |
3 | import { getSimpleShelterProfile } from '@/apis/shelter';
4 |
5 | const useFetchSimpleShelterInfo = (shelterId: number) =>
6 | useSuspenseQuery({
7 | queryKey: ['shelter', 'simpleProfile', shelterId],
8 | queryFn: async () => (await getSimpleShelterProfile(shelterId)).data,
9 | });
10 |
11 | export default useFetchSimpleShelterInfo;
12 |
--------------------------------------------------------------------------------
/apps/volunteer/src/pages/volunteers/index.tsx:
--------------------------------------------------------------------------------
1 | import { useIntersect } from '@anifriends/hooks';
2 | import { Box } from '@chakra-ui/react';
3 | import { useSuspenseInfiniteQuery } from '@tanstack/react-query';
4 | import { MouseEvent, Suspense } from 'react';
5 | import { useNavigate } from 'react-router-dom';
6 |
7 | import VolunteerRecruitItem from '@/pages/volunteers/_components/VolunteerRecruitItem';
8 | import recruitmentQueryOptions from '@/pages/volunteers/_queryOptions/recruitments';
9 | import { createRecruitmentItem } from '@/pages/volunteers/_utils/recruitment';
10 |
11 | import RecruitSkeletonList from './_components/RecruitSkeletonList';
12 |
13 | function Recruitments() {
14 | const navigate = useNavigate();
15 |
16 | const goVolunteersDetail = (event: MouseEvent) => {
17 | const recruitmentId = event.currentTarget.getAttribute('data-id');
18 |
19 | if (recruitmentId) {
20 | navigate(`/volunteers/${recruitmentId}`);
21 | }
22 | };
23 |
24 | const {
25 | data: { pages },
26 | hasNextPage,
27 | isFetchingNextPage,
28 | fetchNextPage,
29 | } = useSuspenseInfiniteQuery(recruitmentQueryOptions.all());
30 |
31 | const recruitments = pages
32 | .flatMap(({ data }) => data.recruitments)
33 | .map(createRecruitmentItem);
34 |
35 | const ref = useIntersect(async (entry, observer) => {
36 | observer.unobserve(entry.target);
37 | if (hasNextPage && !isFetchingNextPage) {
38 | fetchNextPage();
39 | }
40 | });
41 |
42 | return (
43 |
44 | {recruitments.map((recruitment) => (
45 |
50 | ))}
51 | {isFetchingNextPage ? : }
52 |
53 | );
54 | }
55 |
56 | export default function VolunteersPage() {
57 | return (
58 | }>
59 |
60 |
61 | );
62 | }
63 |
--------------------------------------------------------------------------------
/apps/volunteer/src/pages/volunteers/search/_components/RecruitmentsSearchFilter.tsx:
--------------------------------------------------------------------------------
1 | import { SearchFilters, SearchFilterSelectData } from '@anifriends/components';
2 | import { PERIOD } from '@anifriends/constants';
3 | import { ChangeEvent } from 'react';
4 |
5 | import {
6 | RECRUITMENT_STATUS,
7 | SEARCH_TYPE,
8 | } from '@/pages/volunteers/search/_constants/filter';
9 | import { SearchFilter } from '@/pages/volunteers/search/_types/filter';
10 |
11 | type RecruitmentsSearchFilterProps = {
12 | searchFilter: Partial;
13 | onChangeFilter: (event: ChangeEvent) => void;
14 | };
15 |
16 | export default function RecruitmentsSearchFilter({
17 | searchFilter,
18 | onChangeFilter,
19 | }: RecruitmentsSearchFilterProps) {
20 | const searchFilters: SearchFilterSelectData[] = [
21 | {
22 | selectOption: PERIOD,
23 | name: 'period',
24 | placeholder: '봉사일',
25 | value: searchFilter.period,
26 | },
27 | {
28 | selectOption: RECRUITMENT_STATUS,
29 | name: 'recruitmentStatus',
30 | placeholder: '모집',
31 | value: searchFilter.recruitmentStatus,
32 | },
33 | {
34 | selectOption: SEARCH_TYPE,
35 | name: 'searchType',
36 | placeholder: '전체',
37 | value: searchFilter.searchType,
38 | },
39 | ];
40 |
41 | return (
42 |
46 | );
47 | }
48 |
--------------------------------------------------------------------------------
/apps/volunteer/src/pages/volunteers/search/_constants/filter.ts:
--------------------------------------------------------------------------------
1 | export const RECRUITMENT_STATUS = {
2 | IS_OPENED: '모집 중',
3 | IS_CLOSED: '모집 완료',
4 | } as const;
5 |
6 | export const SEARCH_TYPE = {
7 | IS_TITLE: '제목 포함',
8 | IS_CONTENT: '내용 포함',
9 | IS_SHELTER_NAME: '보호소 이름',
10 | } as const;
11 |
--------------------------------------------------------------------------------
/apps/volunteer/src/pages/volunteers/search/_hooks/useRecruitmentSearch.ts:
--------------------------------------------------------------------------------
1 | import { useSearchFilter, useSearchKeyword } from '@anifriends/hooks';
2 | import { ChangeEvent } from 'react';
3 |
4 | import { SearchFilter } from '@/pages/volunteers/search/_types/filter';
5 |
6 | export const useRecruitmentSearch = () => {
7 | const [searchFilter, setSearchFilter] = useSearchFilter();
8 |
9 | const setKeywordFilter = (keyword: string) => setSearchFilter({ keyword });
10 |
11 | useSearchKeyword(setKeywordFilter);
12 |
13 | const handleChangeSearchFilter = (event: ChangeEvent) => {
14 | const { name, value } = event.target;
15 |
16 | setSearchFilter({ [name]: value });
17 | };
18 |
19 | return {
20 | searchFilter,
21 | isKeywordSearched: Boolean(searchFilter.keyword),
22 | handleChangeSearchFilter,
23 | };
24 | };
25 |
--------------------------------------------------------------------------------
/apps/volunteer/src/pages/volunteers/search/_types/filter.ts:
--------------------------------------------------------------------------------
1 | import { Period } from '@anifriends/types';
2 |
3 | import {
4 | RECRUITMENT_STATUS,
5 | SEARCH_TYPE,
6 | } from '@/pages/volunteers/search/_constants/filter';
7 |
8 | export type RecruitmentStatus = keyof typeof RECRUITMENT_STATUS;
9 | export type SearchType = keyof typeof SEARCH_TYPE;
10 |
11 | export type SearchFilter = {
12 | keyword: string;
13 | period: Period;
14 | recruitmentStatus: RecruitmentStatus;
15 | searchType: SearchType;
16 | };
17 |
--------------------------------------------------------------------------------
/apps/volunteer/src/react-query.d.ts:
--------------------------------------------------------------------------------
1 | import '@tanstack/react-query';
2 |
3 | import { AxiosError } from 'axios';
4 | import { ErrorResponseData } from 'shared/types/apis/error';
5 |
6 | declare module '@tanstack/react-query' {
7 | interface Register {
8 | defaultError: AxiosError;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/apps/volunteer/src/types/apis/auth.ts:
--------------------------------------------------------------------------------
1 | import type { PersonGenderEng, SigninRequestData } from '@anifriends/types';
2 |
3 | export type SignupRequestData = SigninRequestData & {
4 | name: string;
5 | birthDate: string;
6 | phoneNumber: string;
7 | gender: PersonGenderEng;
8 | };
9 |
--------------------------------------------------------------------------------
/apps/volunteer/src/types/apis/recruitment.ts:
--------------------------------------------------------------------------------
1 | export type PageInfo = {
2 | totalElements: number;
3 | hasNext: boolean;
4 | };
5 |
6 | export type Pagination = {
7 | size: number;
8 | page: number;
9 | };
10 |
11 | export type Recruitment = {
12 | recruitmentId: number;
13 | recruitmentTitle: string;
14 | recruitmentStartTime: string;
15 | recruitmentEndTime: string;
16 | recruitmentDeadline: string;
17 | recruitmentIsClosed: boolean;
18 | recruitmentApplicantCount: number;
19 | recruitmentCapacity: number;
20 | shelterId: number;
21 | shelterName: string;
22 | shelterImageUrl: string;
23 | };
24 |
25 | export type RecruitementsResponse = {
26 | pageInfo: PageInfo;
27 | recruitments: Recruitment[];
28 | };
29 |
30 | export type RecruitmentSearchFilter = {
31 | keyword: string;
32 | startDate: string;
33 | endDate: string;
34 | closedFilter: 'IS_CLOSED' | 'IS_OPENED';
35 | keywordFilter: 'IS_TITLE' | 'IS_CONTENT' | 'IS_SHELTER_NAME';
36 | };
37 |
38 | export type RecruitmentsRequest = Partial & Pagination;
39 |
--------------------------------------------------------------------------------
/apps/volunteer/src/types/apis/review.ts:
--------------------------------------------------------------------------------
1 | export type PageInfo = {
2 | totalElements: number;
3 | hasNext: boolean;
4 | };
5 |
6 | export type Pagination = {
7 | page: number;
8 | size: number;
9 | };
10 |
11 | export type ReviewDetailResponse = {
12 | reviewId: number;
13 | reviewContent: string;
14 | reviewImageUrls: string[];
15 | };
16 |
17 | export type ReviewCreateRequest = {
18 | applicantId: number;
19 | content: string;
20 | imageUrls: string[];
21 | };
22 |
23 | export type ReviewUpdateRequest = {
24 | content: string;
25 | imageUrls: string[];
26 | };
27 |
28 | export type ReviewsOnShelterRequest = Pagination;
29 |
30 | export type VolunteerReview = {
31 | reviewId: number;
32 | volunteerEmail: string;
33 | volunteerTemperature: number;
34 | reviewCreatedAt: string;
35 | reviewContent: string;
36 | reviewImageUrls: string[];
37 | };
38 |
39 | export type ReviewOnShelterResponse = {
40 | pageInfo: PageInfo;
41 | reviews: VolunteerReview[];
42 | };
43 |
--------------------------------------------------------------------------------
/apps/volunteer/src/types/apis/volunteer.ts:
--------------------------------------------------------------------------------
1 | import { APPLICANT_STATUS } from '@/constants/applicantStatus';
2 |
3 | type PageInfo = {
4 | totalElements: number;
5 | hasNext: boolean;
6 | };
7 |
8 | export type PagenationRequestParams = {
9 | page: number;
10 | size: number;
11 | };
12 |
13 | export type ApplicantStatus = keyof typeof APPLICANT_STATUS;
14 |
15 | export type Applicant = {
16 | shelterId: number;
17 | recruitmentId: number;
18 | recruitmentTitle: string;
19 | recruitmentStartTime: string;
20 | shelterName: string;
21 | applicantId: number;
22 | applicantStatus: ApplicantStatus;
23 | applicantIsWritedReview: boolean;
24 | };
25 |
26 | export type VolunteerApplicantsResponseData = {
27 | pageInfo: PageInfo;
28 | applicants: Applicant[];
29 | };
30 |
--------------------------------------------------------------------------------
/apps/volunteer/src/utils/test/render.tsx:
--------------------------------------------------------------------------------
1 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
2 | import { render } from '@testing-library/react';
3 | import userEvent from '@testing-library/user-event';
4 | import React from 'react';
5 | import { MemoryRouter, RouterProps } from 'react-router-dom';
6 |
7 | const queryClient = new QueryClient({
8 | defaultOptions: {
9 | queries: {
10 | // ✅ turns retries off
11 | retry: false,
12 | },
13 | },
14 | });
15 |
16 | // eslint-disable-next-line react-refresh/only-export-components
17 | export default async (
18 | component: React.ReactNode,
19 | options: { routerProps?: RouterProps },
20 | ) => {
21 | const { routerProps } = options;
22 | const user = userEvent.setup();
23 |
24 | return {
25 | user,
26 | ...render(
27 |
28 | {component}
29 | ,
30 | ),
31 | };
32 | };
33 |
--------------------------------------------------------------------------------
/apps/volunteer/src/utils/test/setupTests.ts:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom';
2 |
3 | afterEach(() => {
4 | vi.clearAllMocks();
5 | });
6 |
7 | afterAll(() => {
8 | vi.resetAllMocks();
9 | });
10 |
11 | // https://github.com/vitest-dev/vitest/issues/821
12 | Object.defineProperty(window, 'matchMedia', {
13 | writable: true,
14 | value: vi.fn().mockImplementation((query) => ({
15 | matches: false,
16 | media: query,
17 | onchange: null,
18 | addListener: vi.fn(), // deprecated
19 | removeListener: vi.fn(), // deprecated
20 | addEventListener: vi.fn(),
21 | removeEventListener: vi.fn(),
22 | dispatchEvent: vi.fn(),
23 | })),
24 | });
25 |
--------------------------------------------------------------------------------
/apps/volunteer/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/apps/volunteer/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@anifriends/tsconfig/vite.json",
3 | "include": ["src"],
4 | "compilerOptions": {
5 | "baseUrl": ".",
6 | "paths": {
7 | "@/*": ["src/*"]
8 | },
9 | "types": ["vitest/globals"]
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/apps/volunteer/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "rewrites": [{ "source": "/(.*)", "destination": "/" }]
3 | }
4 |
--------------------------------------------------------------------------------
/apps/volunteer/vite.config.ts:
--------------------------------------------------------------------------------
1 | import react from '@vitejs/plugin-react-swc';
2 | import { defineConfig } from 'vite';
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig(() => ({
6 | plugins: [react()],
7 | resolve: {
8 | alias: [{ find: '@', replacement: '/src' }],
9 | },
10 | test: {
11 | globals: true,
12 | environment: 'jsdom',
13 | setupFiles: './src/utils/test/setupTests.ts',
14 | },
15 | }));
16 |
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ['@commitlint/config-conventional'],
3 | rules: {
4 | 'subject-case': [0],
5 | 'type-enum': [
6 | 2,
7 | 'always',
8 | [
9 | 'feat',
10 | 'fix',
11 | 'refactor',
12 | 'design',
13 | 'comment',
14 | 'style',
15 | 'test',
16 | 'chore',
17 | 'init',
18 | 'rename',
19 | 'remove',
20 | 'docs',
21 | ],
22 | ],
23 | },
24 | };
25 |
--------------------------------------------------------------------------------
/configs/eslint-config-anifriends/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "eslint-config-anifriends",
3 | "version": "0.0.0",
4 | "license": "MIT",
5 | "main": "index.js",
6 | "dependencies": {
7 | "@typescript-eslint/eslint-plugin": "^5.30.7",
8 | "@typescript-eslint/parser": "^5.30.7",
9 | "eslint-config-prettier": "^8.5.0",
10 | "eslint-plugin-import": "^2.29.0",
11 | "eslint-plugin-prettier": "^5.0.1",
12 | "eslint-plugin-react": "^7.33.2",
13 | "eslint-plugin-react-hooks": "^4.6.0",
14 | "eslint-plugin-react-refresh": "^0.4.3",
15 | "eslint-plugin-simple-import-sort": "^10.0.0",
16 | "eslint-plugin-unused-imports": "^3.0.0"
17 | },
18 | "publishConfig": {
19 | "access": "public"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/configs/tsconfig/base.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "Default",
4 | "compilerOptions": {
5 | "composite": false,
6 | "declaration": true,
7 | "declarationMap": true,
8 | "esModuleInterop": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "inlineSources": false,
11 | "isolatedModules": true,
12 | "moduleResolution": "Bundler",
13 | "noUnusedLocals": false,
14 | "noUnusedParameters": false,
15 | "preserveWatchOutput": true,
16 | "skipLibCheck": true,
17 | "strict": true
18 | },
19 | "exclude": ["node_modules"]
20 | }
21 |
--------------------------------------------------------------------------------
/configs/tsconfig/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@anifriends/tsconfig",
3 | "version": "0.0.0",
4 | "private": true,
5 | "license": "MIT",
6 | "publishConfig": {
7 | "access": "public"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/configs/tsconfig/react-library.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "extends": "./base.json",
4 | "compilerOptions": {
5 | "jsx": "react-jsx",
6 | "lib": ["ESNext", "DOM", "DOM.Iterable"],
7 | "module": "ESNext",
8 | "target": "ES2020"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/configs/tsconfig/vite.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "extends": "./base.json",
4 | "compilerOptions": {
5 | "allowImportingTsExtensions": true,
6 | "jsx": "react-jsx",
7 | "lib": ["ESNext", "DOM", "DOM.Iterable"],
8 | "module": "ESNext",
9 | "noEmit": true,
10 | "noImplicitReturns": true,
11 | "resolveJsonModule": true,
12 | "sourceMap": true,
13 | "target": "ES2020",
14 | "useDefineForClassFields": true
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/cz-config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | types: [
3 | { value: 'feat', name: 'feat:\tfeature를 추가하는 경우' },
4 | { value: 'fix', name: 'fix:\t코드를 수정하는 경우' },
5 | { value: 'docs', name: 'docs:\t문서를 추가하거나 수정하는 경우' },
6 | {
7 | value: 'refactor',
8 | name: 'refactor:\t(버그나 기능 추가 X) 코드를 리팩토링하는 경우',
9 | },
10 | { value: 'design', name: 'design:\tCSS 등 사용자 UI 디자인 변경' },
11 | {
12 | value: 'style',
13 | name: 'style:\t코드 포맷팅, 세미콜론 누락, 코드 변경이 없는 경우',
14 | },
15 | {
16 | value: 'test',
17 | name: 'test:\t(프로덕션 코드 변경 X) 테스트를 추가하거나 테스트 리팩토링하는 경우',
18 | },
19 | {
20 | value: 'chore',
21 | name: 'chore:\t빌드 태스크 업데이트, 패키지 매니저를 설정하는 경우',
22 | },
23 | {
24 | value: 'init',
25 | name: 'init:\t프로젝트 초기 생성',
26 | },
27 | {
28 | value: 'rename',
29 | name: 'rename:\t파일 혹은 폴더명 수정하거나 옮기는 경우',
30 | },
31 | {
32 | value: 'remove',
33 | name: 'remove:\t파일을 삭제하는 작업만 수행하는 경우',
34 | },
35 | ],
36 | scopes: [
37 | { name: 'common' },
38 | { name: 'shared' },
39 | { name: 'volunteer' },
40 | { name: 'shelter' },
41 | ],
42 | allowCustomScopes: false,
43 | allowBreakingChanges: ['feat', 'fix'],
44 | skipQuestions: ['body'],
45 | subjectLimit: 100,
46 | };
47 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "packageManager": "pnpm@8.10.0",
4 | "scripts": {
5 | "build": "dotenv -- turbo run build",
6 | "build:shelter": "dotenv -- turbo run build --filter=shelter",
7 | "build:volunteer": "dotenv -- turbo run build --filter=volunteer",
8 | "commit": "cz",
9 | "dev": "dotenv -- turbo dev --concurrency=17",
10 | "dev:shelter": "dotenv -- turbo dev --filter=shelter",
11 | "dev:volunteer": "dotenv -- turbo dev --filter=volunteer",
12 | "format": "prettier --write \"**/*.{ts,tsx,md}\"",
13 | "lint": "turbo run lint",
14 | "lint:pack": "packlint sort -R",
15 | "prepare": "husky install && packlint sort -R",
16 | "test": "turbo run test",
17 | "test:watch": "turbo run test:watch"
18 | },
19 | "config": {
20 | "commitizen": {
21 | "path": "cz-customizable"
22 | },
23 | "cz-customizable": {
24 | "config": "cz-config.js"
25 | }
26 | },
27 | "devDependencies": {
28 | "@commitlint/cli": "^18.2.0",
29 | "@commitlint/config-conventional": "^18.1.0",
30 | "commitizen": "^4.3.0",
31 | "cz-customizable": "^7.0.0",
32 | "dotenv-cli": "^7.3.0",
33 | "eslint": "^8.45.0",
34 | "eslint-config-anifriends": "workspace:*",
35 | "husky": "^8.0.0",
36 | "lint-staged": "^15.0.2",
37 | "packlint": "^0.2.4",
38 | "prettier": "3.0.3",
39 | "turbo": "latest"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/packages/apis/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@anifriends/apis",
3 | "version": "0.0.0",
4 | "main": "src/index.ts",
5 | "dependencies": {
6 | "@anifriends/constants": "workspace:*",
7 | "@anifriends/store": "workspace:*",
8 | "axios": "^1.6.0"
9 | },
10 | "devDependencies": {
11 | "@anifriends/tsconfig": "workspace:*",
12 | "@anifriends/types": "workspace:*",
13 | "typescript": "^5.0.2"
14 | },
15 | "publishConfig": {
16 | "access": "public"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/packages/apis/src/axiosInterceptor.ts:
--------------------------------------------------------------------------------
1 | import { useAuthStore } from '@anifriends/store';
2 | import { AxiosResponse, InternalAxiosRequestConfig } from 'axios';
3 |
4 | export const onRequest = (config: InternalAxiosRequestConfig) => {
5 | const accessToken = useAuthStore.getState().user?.accessToken;
6 | if (useAuthStore.getState().user?.accessToken) {
7 | config.headers.Authorization = `Bearer ${accessToken}`;
8 | }
9 | return config;
10 | };
11 |
12 | export const onErrorRequest = (error: Error) => {
13 | return Promise.reject(error);
14 | };
15 | export const onResponse = (response: AxiosResponse) => response;
16 | export const onErrorResponse = (error: Error) => {
17 | return Promise.reject(error);
18 | };
19 |
20 | export default {
21 | onRequest,
22 | onErrorRequest,
23 | onResponse,
24 | onErrorResponse,
25 | };
26 |
--------------------------------------------------------------------------------
/packages/apis/src/common/accessToken.ts:
--------------------------------------------------------------------------------
1 | import type { SigninResponseData } from '@anifriends/types';
2 |
3 | import { axiosInstance } from '../axiosInstance';
4 |
5 | export const getAccessTokenAPI = () =>
6 | axiosInstance.post('/auth/refresh');
7 |
--------------------------------------------------------------------------------
/packages/apis/src/common/image.ts:
--------------------------------------------------------------------------------
1 | import { axiosInstance } from '../axiosInstance';
2 |
3 | type uploadImageRequest = FormData;
4 |
5 | type uploadImageResponse = {
6 | imageUrls: string[];
7 | };
8 |
9 | export const uploadImage = (request: FormData) =>
10 | axiosInstance.post(
11 | '/images',
12 | request,
13 | { headers: { 'Content-Type': 'multipart/form-data' } },
14 | );
15 |
--------------------------------------------------------------------------------
/packages/apis/src/common/index.ts:
--------------------------------------------------------------------------------
1 | export * from './accessToken';
2 | export * from './image';
3 | export * from './recruitments';
4 | export * from './review';
5 | export * from './volunteer';
6 |
--------------------------------------------------------------------------------
/packages/apis/src/common/review.ts:
--------------------------------------------------------------------------------
1 | import { axiosInstance } from '../axiosInstance';
2 |
3 | export const getVolunteerReviews = (
4 | volunteerId: number,
5 | page: number,
6 | size: number,
7 | ) =>
8 | axiosInstance.get<
9 | {
10 | pageInfo: {
11 | totalElements: number;
12 | hasNext: boolean;
13 | };
14 | reviews: {
15 | reviewId: number;
16 | shelterName: string;
17 | reviewCreatedAt: string;
18 | reviewContent: string;
19 | reviewImageUrls: string[];
20 | }[];
21 | },
22 | {
23 | page: number;
24 | size: number;
25 | }
26 | >(`/volunteers/${volunteerId}/reviews`, {
27 | params: {
28 | page,
29 | size,
30 | },
31 | });
32 |
--------------------------------------------------------------------------------
/packages/apis/src/common/volunteer.ts:
--------------------------------------------------------------------------------
1 | import { axiosInstance } from '../axiosInstance';
2 |
3 | export const getVolunteerProfileInfo = (recruitmentId: number) =>
4 | axiosInstance.get<{
5 | volunteerEmail: string;
6 | volunteerName: string;
7 | volunteerTemperate: number;
8 | volunteerImageUrl: string;
9 | volunteerPhoneNumber: string;
10 | }>(`/recruitments/${recruitmentId}`);
11 |
--------------------------------------------------------------------------------
/packages/apis/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './axiosInstance';
2 | export * from './axiosInterceptor';
3 | export * from './common';
4 |
--------------------------------------------------------------------------------
/packages/apis/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@anifriends/tsconfig/react-library.json",
3 | "include": ["src"],
4 | "exclude": ["dist", "build", "node_modules"]
5 | }
6 |
--------------------------------------------------------------------------------
/packages/assets/bottomNavBar/icon_mypage_selected.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/packages/assets/bottomNavBar/icon_mypage_unselected.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/packages/assets/bottomNavBar/icon_volunteers_selected.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/packages/assets/bottomNavBar/icon_volunteers_unselected.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/packages/assets/icon-IoEyeSharp.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/packages/assets/icon_BiX.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/packages/assets/icon_IoCamera.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/packages/assets/icon_applicant.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/packages/assets/icon_back.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/packages/assets/icon_menu.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/packages/assets/icon_next.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/packages/assets/icon_notifications.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/packages/assets/icon_review_next.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/packages/assets/icon_search.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/packages/assets/image-anifriends-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anifriends/frontend/8b346b0db582ab9f5894e83c133762b8bdbeae9e/packages/assets/image-anifriends-logo.png
--------------------------------------------------------------------------------
/packages/assets/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@anifriends/assets",
3 | "version": "0.0.0",
4 | "private": true,
5 | "license": "MIT",
6 | "main": "src/index.ts",
7 | "dependencies": {
8 | "react": "^18.2.0"
9 | },
10 | "devDependencies": {
11 | "@anifriends/tsconfig": "workspace:*",
12 | "@types/react": "^18.2.15"
13 | },
14 | "publishConfig": {
15 | "access": "public"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/packages/assets/src/ApplicantIcon.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentProps } from 'react';
2 |
3 | export function ApplicantIcon(props: ComponentProps<'svg'>) {
4 | return (
5 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/packages/assets/src/index.tsx:
--------------------------------------------------------------------------------
1 | export { ApplicantIcon } from './ApplicantIcon';
2 |
--------------------------------------------------------------------------------
/packages/assets/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@anifriends/tsconfig/react-library.json",
3 | "include": ["src"],
4 | "exclude": ["dist", "build", "node_modules"]
5 | }
6 |
--------------------------------------------------------------------------------
/packages/components/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@anifriends/components",
3 | "version": "0.0.0",
4 | "private": true,
5 | "license": "MIT",
6 | "main": "src/index.tsx",
7 | "dependencies": {
8 | "@anifriends/assets": "workspace:*",
9 | "@anifriends/constants": "workspace:*",
10 | "@anifriends/store": "workspace:*",
11 | "@anifriends/utils": "workspace:*",
12 | "@chakra-ui/react": "^2.8.1",
13 | "@emotion/react": "^11.11.1",
14 | "@emotion/styled": "^11.11.0",
15 | "@tanstack/react-query": "^5.4.3",
16 | "framer-motion": "^10.16.4",
17 | "react": "^18.2.0",
18 | "react-error-boundary": "^4.0.11",
19 | "react-router-dom": "^6.17.0"
20 | },
21 | "devDependencies": {
22 | "@anifriends/tsconfig": "workspace:*",
23 | "@types/react": "^18.2.15",
24 | "typescript": "^5.0.2"
25 | },
26 | "publishConfig": {
27 | "access": "public"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/packages/components/png.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.png' {
2 | const content: string;
3 | export default content;
4 | }
5 |
--------------------------------------------------------------------------------
/packages/components/src/AlertModal.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Button,
3 | Modal,
4 | ModalBody,
5 | ModalCloseButton,
6 | ModalContent,
7 | ModalFooter,
8 | ModalHeader,
9 | ModalOverlay,
10 | ModalProps,
11 | } from '@chakra-ui/react';
12 |
13 | type AlertModalProps = {
14 | modalTitle?: string;
15 | modalContent?: string;
16 | btnTitle: string;
17 | onClick?: VoidFunction;
18 | } & Omit;
19 |
20 | export function AlertModal({
21 | modalTitle,
22 | modalContent,
23 | btnTitle,
24 | isOpen,
25 | onClose,
26 | onClick,
27 | }: AlertModalProps) {
28 | return (
29 |
30 |
31 |
32 |
33 | {modalTitle}
34 |
35 |
36 | {modalContent}
37 |
38 |
47 |
57 |
58 |
59 |
60 | );
61 | }
62 |
--------------------------------------------------------------------------------
/packages/components/src/ApplicantStatus.tsx:
--------------------------------------------------------------------------------
1 | import ApplicantIcon from '@anifriends/assets/icon_applicant.svg';
2 | import { Flex, Image, Text } from '@chakra-ui/react';
3 |
4 | type ApplicantStatusProps = {
5 | size?: number;
6 | numerator: number;
7 | denominator: number;
8 | };
9 |
10 | export function ApplicantStatus({
11 | size = 5,
12 | numerator,
13 | denominator,
14 | }: ApplicantStatusProps) {
15 | return (
16 |
17 |
18 |
19 | {`${numerator} / ${denominator}`}
20 |
21 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/packages/components/src/EditPhotoItem.tsx:
--------------------------------------------------------------------------------
1 | import BiX from '@anifriends/assets/icon_BiX.svg';
2 | import NoImage from '@anifriends/assets/icon_no_image.svg';
3 | import type { ImageProps } from '@chakra-ui/react';
4 | import { Box, Image } from '@chakra-ui/react';
5 |
6 | type UploadedPhotoItemProps = {
7 | photoSrc: ImageProps['src'];
8 | onDeletePhoto: VoidFunction;
9 | };
10 |
11 | export function EditPhotoItem({
12 | photoSrc,
13 | onDeletePhoto,
14 | }: UploadedPhotoItemProps) {
15 | return (
16 |
23 |
36 |
37 |
38 |
39 |
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/packages/components/src/FilterGroup.tsx:
--------------------------------------------------------------------------------
1 | import { Flex } from '@chakra-ui/react';
2 | import { ReactNode } from 'react';
3 |
4 | type FilterGroupProps = {
5 | children: ReactNode;
6 | };
7 |
8 | export function FilterGroup({ children }: FilterGroupProps) {
9 | return (
10 |
18 |
26 | {children}
27 |
28 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/packages/components/src/FilterSelect.tsx:
--------------------------------------------------------------------------------
1 | import { Select, SelectProps } from '@chakra-ui/react';
2 |
3 | export function FilterSelect({ children, ...props }: SelectProps) {
4 | return (
5 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/packages/components/src/InfoItem.tsx:
--------------------------------------------------------------------------------
1 | import type { TextProps } from '@chakra-ui/react';
2 | import { Flex, Text } from '@chakra-ui/react';
3 | import { ReactElement } from 'react';
4 |
5 | export type InfoItemStylesProps = {
6 | titleTextStyles?: TextProps;
7 | };
8 |
9 | export type InfoItemProps = {
10 | title: string;
11 | children: ReactElement;
12 | };
13 |
14 | export function InfoItem({
15 | title,
16 | titleTextStyles,
17 | children,
18 | }: InfoItemProps & InfoItemStylesProps) {
19 | return (
20 |
21 |
28 | {title}
29 |
30 | {children}
31 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/packages/components/src/InfoList.tsx:
--------------------------------------------------------------------------------
1 | import { Flex, FlexProps } from '@chakra-ui/react';
2 |
3 | export function InfoList({ children, ...props }: FlexProps) {
4 | return (
5 |
14 | {children}
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/packages/components/src/InfoSubtext.tsx:
--------------------------------------------------------------------------------
1 | import { HStack, Text, TextProps } from '@chakra-ui/react';
2 |
3 | type InfoSubtext = {
4 | title: string;
5 | content: string;
6 | } & Pick;
7 |
8 | export function InfoSubtext({
9 | title,
10 | content,
11 | fontSize = 'xs',
12 | lineHeight = 4,
13 | }: InfoSubtext) {
14 | return (
15 |
16 |
22 | {title}
23 |
24 |
30 | {content}
31 |
32 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/packages/components/src/InfoTextItem.tsx:
--------------------------------------------------------------------------------
1 | import type { TextProps } from '@chakra-ui/react';
2 | import { Text } from '@chakra-ui/react';
3 |
4 | import type { InfoItemStylesProps } from '.';
5 | import { InfoItem } from '.';
6 |
7 | export type InfoTextItemStylesProps = {
8 | contentTextStyles?: TextProps;
9 | } & InfoItemStylesProps;
10 |
11 | export type InfoTextItemProps = {
12 | title: string;
13 | content: string;
14 | };
15 |
16 | export function InfoTextItem({
17 | content,
18 | contentTextStyles,
19 | ...props
20 | }: InfoTextItemProps & InfoTextItemStylesProps) {
21 | return (
22 |
23 |
30 | {content}
31 |
32 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/packages/components/src/InfoTextList.tsx:
--------------------------------------------------------------------------------
1 | import { FlexProps } from '@chakra-ui/react';
2 |
3 | import type { InfoTextItemProps } from '.';
4 | import { InfoList, InfoTextItem } from '.';
5 |
6 | type InfoTextListProps = {
7 | infoTextItems: InfoTextItemProps[];
8 | } & Omit;
9 |
10 | export function InfoTextList({ infoTextItems, ...props }: InfoTextListProps) {
11 | return (
12 |
13 | {infoTextItems.map((infoTextItemProps, index) => (
14 |
15 | ))}
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/packages/components/src/Label.tsx:
--------------------------------------------------------------------------------
1 | import { Badge, BadgeProps } from '@chakra-ui/react';
2 |
3 | const LABEL_BACKGROUND_COLOR = {
4 | GREEN: 'green.300',
5 | ORANGE: 'orange.400',
6 | YELLOW: 'yellow.300',
7 | RED: 'red.400',
8 | GRAY: 'gray.400',
9 | } as const;
10 |
11 | export type LabelProps = {
12 | labelTitle: string;
13 | type?: keyof typeof LABEL_BACKGROUND_COLOR;
14 | } & Pick;
15 |
16 | export function Label({
17 | labelTitle,
18 | type = 'GREEN',
19 | fontSize = 'xs',
20 | lineHeight = 4,
21 | }: LabelProps) {
22 | return (
23 |
34 | {labelTitle}
35 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/packages/components/src/LabelText.tsx:
--------------------------------------------------------------------------------
1 | import { HStack, Text } from '@chakra-ui/react';
2 |
3 | import type { LabelProps } from '.';
4 | import { Label } from '.';
5 |
6 | type LabelTextProps = LabelProps & {
7 | content: string;
8 | };
9 |
10 | export function LabelText({
11 | labelTitle,
12 | type,
13 | content,
14 | fontSize = 'xs',
15 | lineHeight = 4,
16 | }: LabelTextProps) {
17 | return (
18 |
19 |
25 |
30 | {content}
31 |
32 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/packages/components/src/Loader.tsx:
--------------------------------------------------------------------------------
1 | import { Flex, Spinner } from '@chakra-ui/react';
2 |
3 | export function Loader() {
4 | return (
5 |
6 |
13 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/packages/components/src/LogoImageBox.tsx:
--------------------------------------------------------------------------------
1 | import AnimalfriendsLogo from '@anifriends/assets/image-anifriends-logo.png';
2 | import { Center, Image } from '@chakra-ui/react';
3 |
4 | export function LogoImageBox() {
5 | return (
6 |
7 |
8 |
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/packages/components/src/NotReady.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Heading, HStack, Text, VStack } from '@chakra-ui/react';
2 | import { useNavigate } from 'react-router-dom';
3 |
4 | export function NotReady() {
5 | const navigate = useNavigate();
6 |
7 | return (
8 |
9 |
10 | 준비 중입니다
11 |
12 |
13 | 홈 혹은 이전 페이지로 이동해주세요
14 |
15 |
16 |
17 |
18 |
19 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/packages/components/src/OptionMenu.tsx:
--------------------------------------------------------------------------------
1 | import MenuIcon from '@anifriends/assets/icon_menu.svg';
2 | import {
3 | Image,
4 | Menu,
5 | MenuButton,
6 | MenuButtonProps,
7 | MenuItem,
8 | MenuList,
9 | } from '@chakra-ui/react';
10 | import { ReactElement } from 'react';
11 |
12 | type OptionMenuProps = {
13 | children: ReactElement | ReactElement[];
14 | } & Omit;
15 |
16 | export function OptionMenu({ children, ...props }: OptionMenuProps) {
17 | return (
18 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/packages/components/src/ProfileInfo.tsx:
--------------------------------------------------------------------------------
1 | import { Avatar, HStack, Text, VStack } from '@chakra-ui/react';
2 |
3 | type ProfileInfoProps = {
4 | infoImage?: string;
5 | infoTitle: string;
6 | infoTexts?: string[];
7 | children?: React.ReactNode;
8 | };
9 |
10 | export function ProfileInfo({
11 | infoImage,
12 | infoTitle,
13 | infoTexts,
14 | children,
15 | }: ProfileInfoProps) {
16 | return (
17 |
18 |
19 |
20 |
21 |
22 | {infoTitle}
23 |
24 | {children}
25 |
26 | {infoTexts &&
27 | infoTexts.map((infoText, index) => (
28 |
29 | {infoText}
30 |
31 | ))}
32 |
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/packages/components/src/RadioGroup.tsx:
--------------------------------------------------------------------------------
1 | import type {
2 | RadioGroupProps as ChakraRadioGroupProps,
3 | RadioProps,
4 | StackProps,
5 | } from '@chakra-ui/react';
6 | import {
7 | HStack,
8 | Radio,
9 | RadioGroup as ChakraRadioGroup,
10 | } from '@chakra-ui/react';
11 |
12 | type Radio = {
13 | value: Value;
14 | text: Text;
15 | };
16 |
17 | type RadioGroupProps = Omit & {
18 | value: Value;
19 | onChange: (nextValue: Value) => void;
20 | defaultValue?: Value;
21 | radios: Radio[];
22 | hStackProps?: StackProps;
23 | radioProps?: RadioProps;
24 | };
25 |
26 | export function RadioGroup({
27 | value,
28 | onChange,
29 | defaultValue,
30 | radios,
31 | hStackProps,
32 | radioProps,
33 | ...chakraRadioGropRestprops
34 | }: RadioGroupProps) {
35 | return (
36 |
43 |
44 | {radios.map(({ value, text }: Radio) => (
45 |
46 | {text}
47 |
48 | ))}
49 |
50 |
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/packages/components/src/ReviewItemSkeleton.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Card,
3 | HStack,
4 | Skeleton,
5 | SkeletonCircle,
6 | SkeletonText,
7 | Stack,
8 | } from '@chakra-ui/react';
9 |
10 | export function ReviewItemSkeleton() {
11 | return (
12 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/packages/components/src/ReviewItemSkeletonList.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Skeleton, VStack } from '@chakra-ui/react';
2 |
3 | import { ReviewItemSkeleton } from '.';
4 |
5 | export function ReviewItemSkeletonList({
6 | showTitle = false,
7 | }: {
8 | showTitle?: boolean;
9 | }) {
10 | return (
11 |
12 | {showTitle && }
13 |
14 |
15 |
16 |
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/packages/components/src/SearchFilters.tsx:
--------------------------------------------------------------------------------
1 | import { SelectProps } from '@chakra-ui/react';
2 | import { ChangeEvent } from 'react';
3 |
4 | import { FilterGroup, FilterSelect } from '.';
5 |
6 | export type SearchFilterSelectData = {
7 | selectOption: Record;
8 | } & Pick;
9 |
10 | type RecruitmentsSearchFilterProps = {
11 | searchFilters: SearchFilterSelectData[];
12 | onChangeFilter: (event: ChangeEvent) => void;
13 | };
14 |
15 | export function SearchFilters({
16 | searchFilters,
17 | onChangeFilter,
18 | }: RecruitmentsSearchFilterProps) {
19 | return (
20 |
21 | {searchFilters.map(({ selectOption, name, ...props }) => (
22 |
28 | {Object.entries(selectOption).map(([key, value]) => (
29 |
32 | ))}
33 |
34 | ))}
35 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/packages/components/src/SettingGroup.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Text } from '@chakra-ui/react';
2 |
3 | import type { SettingItemProps } from '.';
4 | import { SettingItem } from '.';
5 |
6 | type SettingProps = {
7 | groupTitle: string;
8 | settingItems: SettingItemProps[];
9 | };
10 |
11 | export function SettingGroup({ groupTitle, settingItems }: SettingProps) {
12 | return (
13 |
14 |
15 | {groupTitle}
16 |
17 | {settingItems.map((item, index) => (
18 |
19 | ))}
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/packages/components/src/SettingItem.tsx:
--------------------------------------------------------------------------------
1 | import Next from '@anifriends/assets/icon_next.svg';
2 | import { Box, Flex, Image, Text } from '@chakra-ui/react';
3 |
4 | export type SettingItemProps = {
5 | itemTitle: string;
6 | onClick: VoidFunction;
7 | };
8 | export function SettingItem({ itemTitle, onClick }: SettingItemProps) {
9 | return (
10 |
19 | {itemTitle}
20 |
21 |
22 |
23 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/packages/components/src/Tabs.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Tab,
3 | TabList,
4 | TabPanel,
5 | TabPanels,
6 | Tabs as ChakraTabs,
7 | } from '@chakra-ui/react';
8 | import { type ReactNode, useState } from 'react';
9 | import { useSearchParams } from 'react-router-dom';
10 |
11 | type TabName = string;
12 |
13 | type TabNode = ReactNode;
14 |
15 | type TabsProps = {
16 | tabs: [TabName, TabNode][];
17 | };
18 |
19 | export function Tabs({ tabs }: TabsProps) {
20 | const [searchParams, setSearchParams] = useSearchParams();
21 | const tabId = Number(searchParams.get('tab'));
22 | const [tabIndex, setTabIndex] = useState(tabId);
23 |
24 | const handleTabsChange = (index: number) => setTabIndex(index);
25 |
26 | const handleSetTabParam = () =>
27 | setSearchParams({ tab: `${tabIndex}` }, { replace: true });
28 |
29 | return (
30 |
38 |
39 | {tabs.map((tab, index) => (
40 |
58 | {tab[0]}
59 |
60 | ))}
61 |
62 |
63 | {tabs.map((tab, index) => (
64 | {tab[1]}
65 | ))}
66 |
67 |
68 | );
69 | }
70 |
--------------------------------------------------------------------------------
/packages/components/src/WithLogin.tsx:
--------------------------------------------------------------------------------
1 | import { useAuthStore } from '@anifriends/store';
2 | import { ReactNode } from 'react';
3 | import { Navigate } from 'react-router-dom';
4 |
5 | export function WithLogin({ children }: { children: ReactNode }) {
6 | const { user } = useAuthStore();
7 |
8 | if (user) {
9 | return children;
10 | }
11 |
12 | return ;
13 | }
14 |
--------------------------------------------------------------------------------
/packages/components/src/index.tsx:
--------------------------------------------------------------------------------
1 | export { AlertModal } from './AlertModal';
2 | export { ApplicantStatus } from './ApplicantStatus';
3 | export { EditPhotoItem } from './EditPhotoItem';
4 | export type { Photo } from './EditPhotoList';
5 | export { EditPhotoList } from './EditPhotoList';
6 | export { FilterGroup } from './FilterGroup';
7 | export { FilterSelect } from './FilterSelect';
8 | export { ImageCarousel } from './ImageCarousel';
9 | export type { InfoItemProps, InfoItemStylesProps } from './InfoItem';
10 | export { InfoItem } from './InfoItem';
11 | export { InfoList } from './InfoList';
12 | export { InfoSubtext } from './InfoSubtext';
13 | export type {
14 | InfoTextItemProps,
15 | InfoTextItemStylesProps,
16 | } from './InfoTextItem';
17 | export { InfoTextItem } from './InfoTextItem';
18 | export { InfoTextList } from './InfoTextList';
19 | export type { LabelProps } from './Label';
20 | export { Label } from './Label';
21 | export { LabelText } from './LabelText';
22 | export { Loader } from './Loader';
23 | export { LocalErrorBoundary } from './LocalErrorBoundary';
24 | export { LogoImageBox } from './LogoImageBox';
25 | export { NotReady } from './NotReady';
26 | export { OptionMenu } from './OptionMenu';
27 | export { ProfileInfo } from './ProfileInfo';
28 | export { RadioGroup } from './RadioGroup';
29 | export { ReviewItem } from './ReviewItem';
30 | export { ReviewItemSkeleton } from './ReviewItemSkeleton';
31 | export { ReviewItemSkeletonList } from './ReviewItemSkeletonList';
32 | export type { SearchFilterSelectData } from './SearchFilters';
33 | export { SearchFilters } from './SearchFilters';
34 | export { SettingGroup } from './SettingGroup';
35 | export type { SettingItemProps } from './SettingItem';
36 | export { SettingItem } from './SettingItem';
37 | export { Tabs } from './Tabs';
38 | export { WithLogin } from './WithLogin';
39 |
--------------------------------------------------------------------------------
/packages/components/svg.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.svg' {
2 | const content: string;
3 | export default content;
4 | }
5 |
--------------------------------------------------------------------------------
/packages/components/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@anifriends/tsconfig/react-library.json",
3 | "include": ["src", "svg.d.ts", "png.d.ts"],
4 | "exclude": ["dist", "build", "node_modules"]
5 | }
6 |
--------------------------------------------------------------------------------
/packages/constants/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@anifriends/constants",
3 | "version": "0.0.0",
4 | "private": true,
5 | "license": "MIT",
6 | "main": "src/index.ts",
7 | "devDependencies": {
8 | "@anifriends/tsconfig": "workspace:*",
9 | "@anifriends/types": "workspace:*",
10 | "typescript": "^5.0.2"
11 | },
12 | "publishConfig": {
13 | "access": "public"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/packages/constants/src/appType.ts:
--------------------------------------------------------------------------------
1 | export const APP_TYPE = {
2 | SHELTER_APP: 'SHELTER_APP',
3 | VOLUNTEER_APP: 'VOLUNTEER_APP',
4 | } as const;
5 |
--------------------------------------------------------------------------------
/packages/constants/src/baseURL.ts:
--------------------------------------------------------------------------------
1 | const DEV_BASE_URL = import.meta.env.VITE_BASE_URL;
2 |
3 | export const BASE_URL = import.meta.env.PROD
4 | ? import.meta.env.VITE_BASE_URL
5 | : DEV_BASE_URL;
6 |
--------------------------------------------------------------------------------
/packages/constants/src/date.ts:
--------------------------------------------------------------------------------
1 | export const MILISECONDS = {
2 | YEAR: 60 * 60 * 24 * 365,
3 | MONTH: 60 * 60 * 24 * 30,
4 | DAY: 60 * 60 * 24,
5 | HOUR: 60 * 60,
6 | MINUITE: 60,
7 | };
8 |
9 | export const WEEK_DAYS = ['일', '월', '화', '수', '목', '금', '토'];
10 |
--------------------------------------------------------------------------------
/packages/constants/src/gender.ts:
--------------------------------------------------------------------------------
1 | export const PERSON_GENDER_ENG = {
2 | FEMALE: 'FEMALE',
3 | MALE: 'MALE',
4 | } as const;
5 |
6 | export const PERSON_GENDER_KOR = {
7 | FEMALE: '여성',
8 | MALE: '남성',
9 | } as const;
10 |
--------------------------------------------------------------------------------
/packages/constants/src/headerTitle.ts:
--------------------------------------------------------------------------------
1 | import { PageType } from '@anifriends/types';
2 |
3 | type HeaderTitle = {
4 | [key in PageType]: string;
5 | };
6 |
7 | export const headerTitle: HeaderTitle = {
8 | VOLUNTEERS: '봉사자 모집',
9 | VOLUNTEERS_DETAIL: '봉사자 모집 상세',
10 | VOLUNTEERS_PROFILE: '봉사자 프로필',
11 | VOLUNTEERS_SEARCH: '봉사자 모집글 검색',
12 | VOLUNTEERS_WRITE: '봉사자 모집글 작성',
13 | VOLUNTEERS_UPDATE: '봉사자 모집글 수정',
14 | ANIMALS: '유기보호 동물',
15 | ANIMALS_DETAIL: '유기보호 동물 상세',
16 | ANIMALS_SEARCH: '유기보호 동물 검색',
17 | ANIMALS_WRITE: '유기보호 동물 작성',
18 | ANIMALS_UPDATE: '유기보호 동물 수정',
19 | CHATTINGS: '채팅',
20 | CHATTINGS_ROOM: '채팅방',
21 | MYPAGE: '마이페이지',
22 | MYPAGE_REVIEWS: '봉사 후기',
23 | SETTINGS: '설정',
24 | SETTINGS_ACCOUNT: '계정 정보 수정',
25 | SETTINGS_PASSWORD: '비밀 번호 수정',
26 | MANAGE_ATTENDANCE: '봉사자 출석 관리',
27 | MANAGE_APPLY: '봉사자 신청 현황',
28 | NOTIFICATIONS: '알림',
29 | SHELTERS_PROFILE: '보호소 프로필',
30 | SHELTERS_REVIEWS_WRITE: '봉사 후기 작성',
31 | SHELTERS_REVIEWS_UPDATE: '봉사 후기 수정',
32 | SIGNUP: '회원가입',
33 | SIGNIN: '로그인',
34 | } as const;
35 |
--------------------------------------------------------------------------------
/packages/constants/src/headerType.ts:
--------------------------------------------------------------------------------
1 | export const HEADER_TYPE = {
2 | DEFAULT: 'DEFAULT',
3 | DETAIL: 'DETAIL',
4 | SEARCH: 'SEARCH',
5 | } as const;
6 |
--------------------------------------------------------------------------------
/packages/constants/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './appType';
2 | export * from './baseURL';
3 | export * from './date';
4 | export * from './gender';
5 | export * from './headerTitle';
6 | export * from './headerType';
7 | export * from './pageType';
8 | export * from './period';
9 |
--------------------------------------------------------------------------------
/packages/constants/src/pageType.ts:
--------------------------------------------------------------------------------
1 | export const PAGE_TYPE = {
2 | VOLUNTEERS: 'VOLUNTEERS',
3 | VOLUNTEERS_DETAIL: 'VOLUNTEERS_DETAIL',
4 | VOLUNTEERS_PROFILE: 'VOLUNTEERS_PROFILE',
5 | VOLUNTEERS_SEARCH: 'VOLUNTEERS_SEARCH',
6 | VOLUNTEERS_WRITE: 'VOLUNTEERS_WRITE',
7 | VOLUNTEERS_UPDATE: 'VOLUNTEERS_UPDATE',
8 | ANIMALS: 'ANIMALS',
9 | ANIMALS_DETAIL: 'ANIMALS_DETAIL',
10 | ANIMALS_SEARCH: 'ANIMALS_SEARCH',
11 | ANIMALS_WRITE: 'ANIMALS_WRITE',
12 | ANIMALS_UPDATE: 'ANIMALS_UPDATE',
13 | CHATTINGS: 'CHATTINGS',
14 | CHATTINGS_ROOM: 'CHATTINGS_ROOM',
15 | MYPAGE: 'MYPAGE',
16 | MYPAGE_REVIEWS: 'MYPAGE_REVIEWS',
17 | SETTINGS: 'SETTINGS',
18 | SETTINGS_ACCOUNT: 'SETTINGS_ACCOUNT',
19 | SETTINGS_PASSWORD: 'SETTINGS_PASSWORD',
20 | MANAGE_ATTENDANCE: 'MANAGE_ATTENDANCE',
21 | MANAGE_APPLY: 'MANAGE_APPLY',
22 | NOTIFICATIONS: 'NOTIFICATIONS',
23 | SHELTERS_PROFILE: 'SHELTERS_PROFILE',
24 | SHELTERS_REVIEWS_WRITE: 'SHELTERS_REVIEWS_WRITE',
25 | SHELTERS_REVIEWS_UPDATE: 'SHELTERS_REVIEWS_UPDATE',
26 | SIGNUP: 'SIGNUP',
27 | SIGNIN: 'SIGNIN',
28 | } as const;
29 |
--------------------------------------------------------------------------------
/packages/constants/src/period.ts:
--------------------------------------------------------------------------------
1 | export const PERIOD = {
2 | WITHIN_ONE_DAY: '1일 이내',
3 | WITHIN_ONE_WEEK: '1주 이내',
4 | WITHIN_ONE_MONTH: '1달 이내',
5 | WITHIN_THREE_MONTH: '3달 이내',
6 | } as const;
7 |
--------------------------------------------------------------------------------
/packages/constants/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@anifriends/tsconfig/react-library.json",
3 | "include": ["src"],
4 | "exclude": ["dist", "build", "node_modules"]
5 | }
6 |
--------------------------------------------------------------------------------
/packages/fonts/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@anifriends/fonts",
3 | "version": "0.0.0",
4 | "private": true,
5 | "license": "MIT",
6 | "main": "src/index.tsx",
7 | "dependencies": {
8 | "@emotion/react": "^11.11.1",
9 | "react": "^18.2.0"
10 | },
11 | "devDependencies": {
12 | "@anifriends/tsconfig": "workspace:*",
13 | "@types/react": "^18.2.15",
14 | "typescript": "^5.0.2"
15 | },
16 | "publishConfig": {
17 | "access": "public"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/packages/fonts/src/index.tsx:
--------------------------------------------------------------------------------
1 | import { Global } from '@emotion/react';
2 |
3 | export default function Fonts() {
4 | return (
5 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/packages/fonts/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@anifriends/tsconfig/react-library.json",
3 | "include": ["src"],
4 | "exclude": ["dist", "build", "node_modules"]
5 | }
6 |
--------------------------------------------------------------------------------
/packages/hooks/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@anifriends/hooks",
3 | "version": "0.0.0",
4 | "main": "src/index.ts",
5 | "dependencies": {
6 | "@anifriends/apis": "workspace:*",
7 | "@anifriends/components": "workspace:*",
8 | "@anifriends/store": "workspace:*",
9 | "@anifriends/types": "workspace:*",
10 | "@anifriends/utils": "workspace:*",
11 | "@chakra-ui/react": "^2.8.1",
12 | "@emotion/react": "^11.11.1",
13 | "@emotion/styled": "^11.11.0",
14 | "@tanstack/react-query": "^5.4.3",
15 | "framer-motion": "^10.16.4",
16 | "react": "^18.2.0",
17 | "react-router-dom": "^6.17.0"
18 | },
19 | "devDependencies": {
20 | "@anifriends/tsconfig": "workspace:*",
21 | "@types/react": "^18.2.15",
22 | "typescript": "^5.0.2"
23 | },
24 | "publishConfig": {
25 | "access": "public"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/packages/hooks/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './useAccessTokenMutation';
2 | export * from './useIntersect';
3 | export * from './usePageType';
4 | export * from './usePhotosUpload';
5 | export * from './usePhotoUpload';
6 | export * from './useRadioGroup';
7 | export * from './useSearchFilter';
8 | export * from './useSearchKeyword';
9 | export * from './useToggle';
10 |
--------------------------------------------------------------------------------
/packages/hooks/src/useAccessTokenMutation.ts:
--------------------------------------------------------------------------------
1 | import { getAccessTokenAPI } from '@anifriends/apis';
2 | import { useAuthStore } from '@anifriends/store';
3 | import { useMutation } from '@tanstack/react-query';
4 |
5 | export const useAccessTokenMutation = () => {
6 | const { setUser } = useAuthStore();
7 |
8 | return useMutation({
9 | mutationFn: async () => {
10 | const { data } = await getAccessTokenAPI();
11 | return data;
12 | },
13 | onSuccess: ({ accessToken, userId }) => {
14 | setUser({
15 | accessToken,
16 | userId,
17 | });
18 | },
19 | onError: (error) => {
20 | console.warn(error);
21 | setUser(null);
22 | },
23 | });
24 | };
25 |
--------------------------------------------------------------------------------
/packages/hooks/src/useIntersect.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useRef } from 'react';
2 |
3 | type IntersectHandler = (
4 | entry: IntersectionObserverEntry,
5 | observer: IntersectionObserver,
6 | ) => void;
7 |
8 | export const useIntersect = (
9 | onIntersect: IntersectHandler,
10 | options?: IntersectionObserverInit,
11 | ) => {
12 | const ref = useRef(null);
13 | const callback = useCallback(
14 | (entries: IntersectionObserverEntry[], observer: IntersectionObserver) => {
15 | entries.forEach((entry) => {
16 | if (entry.isIntersecting) {
17 | onIntersect(entry, observer);
18 | }
19 | });
20 | },
21 | [onIntersect],
22 | );
23 |
24 | useEffect(() => {
25 | if (!ref.current) {
26 | return;
27 | }
28 | const observer = new IntersectionObserver(callback, options);
29 | observer.observe(ref.current);
30 | return () => observer.disconnect();
31 | }, [ref, options, callback]);
32 |
33 | return ref;
34 | };
35 |
36 | export default useIntersect;
37 |
--------------------------------------------------------------------------------
/packages/hooks/src/usePageType.ts:
--------------------------------------------------------------------------------
1 | import { PageType } from '@anifriends/types';
2 | import { useEffect, useState } from 'react';
3 | import { useMatches } from 'react-router-dom';
4 |
5 | export const usePageType = () => {
6 | const [pageType, setPageType] = useState();
7 |
8 | const match = useMatches().at(-1);
9 |
10 | useEffect(() => {
11 | const page = match?.id;
12 |
13 | setPageType(page as PageType);
14 | }, [match]);
15 |
16 | return { pageType };
17 | };
18 |
--------------------------------------------------------------------------------
/packages/hooks/src/usePhotoUpload.ts:
--------------------------------------------------------------------------------
1 | import { uploadImage } from '@anifriends/apis';
2 | import { resizeImageFile } from '@anifriends/utils';
3 | import { useToast, UseToastOptions } from '@chakra-ui/react';
4 | import { useState } from 'react';
5 |
6 | const toastOption = (
7 | description: string,
8 | status: UseToastOptions['status'],
9 | ): UseToastOptions => {
10 | return {
11 | description,
12 | position: 'top',
13 | status,
14 | duration: 1500,
15 | isClosable: true,
16 | };
17 | };
18 |
19 | export const usePhotoUpload = (initialPhoto?: string) => {
20 | const [photo, setPhoto] = useState(initialPhoto);
21 |
22 | const toast = useToast();
23 |
24 | const handleUploadPhoto = async (files: FileList | null) => {
25 | if (!files) {
26 | return;
27 | }
28 |
29 | if (files.length !== 1) {
30 | toast(toastOption('프로필 이미지는 한 장만 등록 가능합니다.', 'error'));
31 |
32 | return;
33 | }
34 |
35 | const imageFile = files[0];
36 | const localImageUrl = URL.createObjectURL(imageFile);
37 | setPhoto(localImageUrl);
38 |
39 | const resizedImage = await resizeImageFile(imageFile, 2);
40 | const formData = new FormData();
41 | formData.append('images', resizedImage);
42 |
43 | const { data } = await uploadImage(formData);
44 | const [imageUrl] = data.imageUrls;
45 |
46 | setPhoto(imageUrl);
47 | };
48 |
49 | const handleDeletePhoto = () => {
50 | setPhoto(undefined);
51 | };
52 |
53 | return {
54 | photo,
55 | setPhoto,
56 | handleUploadPhoto,
57 | handleDeletePhoto,
58 | };
59 | };
60 |
--------------------------------------------------------------------------------
/packages/hooks/src/useRadioGroup.ts:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 |
3 | export const useRadioGroup = (
4 | initialValue: Value,
5 | ): [Value, (nextValue: Value) => void] => {
6 | const [value, setValue] = useState(initialValue);
7 |
8 | const changeValue = (nextValue: Value) => setValue(nextValue);
9 |
10 | return [value, changeValue];
11 | };
12 |
--------------------------------------------------------------------------------
/packages/hooks/src/useSearchFilter.ts:
--------------------------------------------------------------------------------
1 | import { useSearchHeaderStore } from '@anifriends/store';
2 | import { useEffect, useState } from 'react';
3 | import { useSearchParams } from 'react-router-dom';
4 |
5 | type KeywordFilter = {
6 | keyword: string;
7 | };
8 |
9 | const parseSearchParams = (
10 | searchParams: URLSearchParams,
11 | ): Partial => {
12 | const searchFilter: Partial = {};
13 |
14 | for (const [key, value] of searchParams) {
15 | searchFilter[key as keyof SearchFilter] =
16 | value as SearchFilter[keyof SearchFilter];
17 | }
18 |
19 | return searchFilter;
20 | };
21 |
22 | const serializeSearchFilter = (
23 | searchFilter: Partial,
24 | ): URLSearchParams => {
25 | const searchFilterEntries = Object.entries(searchFilter);
26 | const searchParams = new URLSearchParams();
27 |
28 | for (const [key, value] of searchFilterEntries) {
29 | if (value) {
30 | searchParams.append(key, String(value));
31 | }
32 | }
33 |
34 | return searchParams;
35 | };
36 |
37 | export const useSearchFilter = (
38 | filter: Partial = {},
39 | ): [Partial, (filter: Partial) => void] => {
40 | const setKeyword = useSearchHeaderStore((state) => state.setKeyword);
41 |
42 | const [searchFilter, setSearchFilter] =
43 | useState>(filter);
44 | const [searchParams, setSearchParams] = useSearchParams();
45 |
46 | useEffect(() => {
47 | const params: Partial = parseSearchParams(searchParams);
48 |
49 | if (params.keyword) {
50 | setKeyword(params.keyword);
51 | }
52 |
53 | setSearchFilter(params);
54 | }, [searchParams]);
55 |
56 | const setSearchFilterValue = (filterValue: Partial) => {
57 | const newSearchFilter = { ...searchFilter, ...filterValue };
58 | const newSearchParams = serializeSearchFilter(newSearchFilter);
59 |
60 | setSearchFilter(newSearchFilter);
61 | setSearchParams(newSearchParams, { replace: true });
62 | };
63 |
64 | return [searchFilter, setSearchFilterValue];
65 | };
66 |
--------------------------------------------------------------------------------
/packages/hooks/src/useSearchKeyword.ts:
--------------------------------------------------------------------------------
1 | import { useSearchHeaderStore } from '@anifriends/store';
2 | import { useEffect } from 'react';
3 |
4 | export const useSearchKeyword = (
5 | setKeywordFilter: (keyword: string) => void,
6 | ) => {
7 | const [setOnSearch, setKeyword, keyword] = useSearchHeaderStore((state) => [
8 | state.setOnSearch,
9 | state.setKeyword,
10 | state.keyword,
11 | ]);
12 |
13 | useEffect(() => {
14 | setOnSearch(setKeywordFilter);
15 |
16 | return () => {
17 | setKeyword('');
18 | setOnSearch(() => {});
19 | };
20 | }, []);
21 |
22 | return keyword;
23 | };
24 |
--------------------------------------------------------------------------------
/packages/hooks/src/useToggle.ts:
--------------------------------------------------------------------------------
1 | import { useBoolean } from '@chakra-ui/react';
2 |
3 | export const useToggle = (initialState = false): [boolean, () => void] => {
4 | const [isTrue, setIsTrue] = useBoolean(initialState);
5 |
6 | const toggle = () => {
7 | setIsTrue.toggle();
8 | };
9 |
10 | return [isTrue, toggle];
11 | };
12 |
--------------------------------------------------------------------------------
/packages/hooks/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@anifriends/tsconfig/react-library.json",
3 | "include": ["src"],
4 | "exclude": ["dist", "build", "node_modules"]
5 | }
6 |
--------------------------------------------------------------------------------
/packages/icons/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@anifriends/icons",
3 | "version": "0.0.0",
4 | "private": true,
5 | "license": "MIT",
6 | "main": "src/index.tsx",
7 | "dependencies": {
8 | "react": "^18.2.0"
9 | },
10 | "devDependencies": {
11 | "@anifriends/tsconfig": "workspace:*",
12 | "@types/react": "^18.2.15"
13 | },
14 | "publishConfig": {
15 | "access": "public"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/packages/icons/src/ApplicantIcon.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentProps } from 'react';
2 |
3 | export function ApplicantIcon(props: ComponentProps<'svg'>) {
4 | return (
5 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/packages/icons/src/BackIcon.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentProps } from 'react';
2 |
3 | export function BackIcon(props: ComponentProps<'svg'>) {
4 | return (
5 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/packages/icons/src/BiXIcon.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentProps } from 'react';
2 |
3 | export function BiXIcon(props: ComponentProps<'svg'>) {
4 | return (
5 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/packages/icons/src/IoCameraIcon.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentProps } from 'react';
2 |
3 | export function IoCameraIcon(props: Omit, 'children'>) {
4 | return (
5 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/packages/icons/src/IoEyeSharp.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentProps } from 'react';
2 |
3 | export function IoEyeSharp({ ...props }: ComponentProps<'svg'>) {
4 | return (
5 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/packages/icons/src/MenuIcon.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentProps } from 'react';
2 |
3 | export function MenuIcon(props: ComponentProps<'svg'>) {
4 | return (
5 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/packages/icons/src/NextIcon.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentProps } from 'react';
2 |
3 | export function NextIcon(props: ComponentProps<'svg'>) {
4 | return (
5 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/packages/icons/src/NotificationIcon.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentProps } from 'react';
2 |
3 | export function NotificationIcon(props: ComponentProps<'svg'>) {
4 | return (
5 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/packages/icons/src/ReviewNextIcon.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentProps } from 'react';
2 |
3 | export function ReviewNextIcon(props: ComponentProps<'svg'>) {
4 | return (
5 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/packages/icons/src/SearchIcon.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentProps } from 'react';
2 |
3 | export function SearchIcon(props: ComponentProps<'svg'>) {
4 | return (
5 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/packages/icons/src/index.tsx:
--------------------------------------------------------------------------------
1 | export { ApplicantIcon } from './ApplicantIcon';
2 | export { BackIcon } from './BackIcon';
3 | export { BiXIcon } from './BiXIcon';
4 | export { IoCameraIcon } from './IoCameraIcon';
5 | export { IoEyeOff } from './IoEyeOff';
6 | export { IoEyeSharp } from './IoEyeSharp';
7 | export { MenuIcon } from './MenuIcon';
8 | export { NextIcon } from './NextIcon';
9 | export { NoImageIcon } from './NoImageIcon';
10 | export { NotificationIcon } from './NotificationIcon';
11 | export { ReviewNextIcon } from './ReviewNextIcon';
12 | export { SearchIcon } from './SearchIcon';
13 | export { SettingsIcon } from './SettingsIcon';
14 |
--------------------------------------------------------------------------------
/packages/icons/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@anifriends/tsconfig/react-library.json",
3 | "include": ["src"],
4 | "exclude": ["dist", "build", "node_modules"]
5 | }
6 |
--------------------------------------------------------------------------------
/packages/layout/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@anifriends/layout",
3 | "version": "0.0.0",
4 | "private": true,
5 | "license": "MIT",
6 | "main": "src/index.tsx",
7 | "dependencies": {
8 | "@anifriends/assets": "workspace:*",
9 | "@anifriends/components": "workspace:*",
10 | "@anifriends/constants": "workspace:*",
11 | "@anifriends/hooks": "workspace:*",
12 | "@anifriends/store": "workspace:*",
13 | "@anifriends/utils": "workspace:*",
14 | "@chakra-ui/react": "^2.8.1",
15 | "@emotion/react": "^11.11.1",
16 | "@emotion/styled": "^11.11.0",
17 | "framer-motion": "^10.16.4",
18 | "react": "^18.2.0",
19 | "react-dom": "^18.2.0",
20 | "react-router-dom": "^6.17.0"
21 | },
22 | "devDependencies": {
23 | "@anifriends/tsconfig": "workspace:*",
24 | "@anifriends/types": "workspace:*",
25 | "@types/react": "^18.2.15",
26 | "@types/react-dom": "^18.2.7",
27 | "typescript": "^5.0.2"
28 | },
29 | "publishConfig": {
30 | "access": "public"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/packages/layout/src/BottomNavBar/NavBarButton.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Image } from '@chakra-ui/react';
2 |
3 | export type NavBarButtonProps = {
4 | onClick: VoidFunction;
5 | selected: boolean;
6 | buttonImageSrc: [string, string];
7 | buttonText: string;
8 | };
9 |
10 | export default function NavBarButton({
11 | onClick,
12 | selected,
13 | buttonImageSrc,
14 | buttonText,
15 | }: NavBarButtonProps) {
16 | const [unselectedButton, selectedButton] = buttonImageSrc;
17 |
18 | return (
19 |
25 |
26 | {buttonText}
27 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/packages/layout/src/BottomNavBar/useBottomNavBar.ts:
--------------------------------------------------------------------------------
1 | import { PAGE_TYPE } from '@anifriends/constants';
2 | import { PageType } from '@anifriends/types';
3 | import { useEffect, useState } from 'react';
4 |
5 | export const useBottomNavBar = (pageType?: PageType) => {
6 | const [isBottomNavBarVisible, setIsBottomNavBarVisible] = useState(false);
7 |
8 | useEffect(() => {
9 | if (
10 | pageType === PAGE_TYPE.VOLUNTEERS ||
11 | pageType === PAGE_TYPE.ANIMALS ||
12 | pageType === PAGE_TYPE.CHATTINGS ||
13 | pageType === PAGE_TYPE.MYPAGE
14 | ) {
15 | return setIsBottomNavBarVisible(true);
16 | }
17 |
18 | return setIsBottomNavBarVisible(false);
19 | }, [pageType]);
20 |
21 | return { isBottomNavBarVisible };
22 | };
23 |
--------------------------------------------------------------------------------
/packages/layout/src/Header/DefaultHeader/useDefaultHeader.ts:
--------------------------------------------------------------------------------
1 | import { usePageType } from '@anifriends/hooks';
2 | import { AppType } from '@anifriends/types';
3 | import { useEffect, useState } from 'react';
4 |
5 | import { getHeaderTitle } from '../utils';
6 | import defaultHeaderState from './headerIconState';
7 |
8 | export type DefaultHeaderIconVisibility = {
9 | searchIcon: boolean;
10 | settingsIcon: boolean;
11 | notificationsIcon: boolean;
12 | };
13 |
14 | export const useDefaultHeader = (appType: AppType) => {
15 | const { pageType } = usePageType();
16 | const [title, setTitle] = useState('');
17 | const [iconVisibility, setIconVisibility] =
18 | useState({
19 | searchIcon: false,
20 | settingsIcon: false,
21 | notificationsIcon: false,
22 | });
23 |
24 | useEffect(() => {
25 | if (pageType) {
26 | setTitle(getHeaderTitle(pageType));
27 |
28 | const iconState = defaultHeaderState[pageType];
29 |
30 | if (iconState) {
31 | setIconVisibility(iconState[appType]);
32 | }
33 | }
34 | }, [appType, pageType]);
35 |
36 | return { title, iconVisibility };
37 | };
38 |
--------------------------------------------------------------------------------
/packages/layout/src/Header/DetailHeader/index.tsx:
--------------------------------------------------------------------------------
1 | import BackIcon from '@anifriends/assets/icon_back.svg';
2 | import { OptionMenu } from '@anifriends/components';
3 | import { useDetailHeaderStore } from '@anifriends/store';
4 | import { Box, Flex, Image, MenuItem, Text } from '@chakra-ui/react';
5 | import { useLocation, useNavigate } from 'react-router-dom';
6 |
7 | import { HeaderProps } from '../index';
8 | import { useDetailHeader } from './useDetailHeader';
9 |
10 | export default function DetailHeader({ appType }: HeaderProps) {
11 | const navigate = useNavigate();
12 | const { pathname } = useLocation();
13 | const { title, iconVisibility } = useDetailHeader(appType);
14 | const onDelete = useDetailHeaderStore((state) => state.onDelete);
15 |
16 | const { menuIcon } = iconVisibility;
17 |
18 | const goBack = () => navigate(-1);
19 |
20 | const handleUpdate = () => {
21 | const [, path, id] = pathname.split('/');
22 |
23 | navigate(`${path}/write/${id}`);
24 | };
25 |
26 | const handleDelete = () => {
27 | const [, , id] = pathname.split('/');
28 |
29 | onDelete(Number(id));
30 | };
31 |
32 | return (
33 |
47 |
48 |
49 |
50 |
51 | {title}
52 |
53 | {menuIcon && (
54 |
55 |
56 |
57 |
58 | )}
59 |
60 | );
61 | }
62 |
--------------------------------------------------------------------------------
/packages/layout/src/Header/DetailHeader/useDetailHeader.ts:
--------------------------------------------------------------------------------
1 | import { APP_TYPE, PAGE_TYPE } from '@anifriends/constants';
2 | import { usePageType } from '@anifriends/hooks';
3 | import { AppType } from '@anifriends/types';
4 | import { useEffect, useState } from 'react';
5 |
6 | import { getHeaderTitle } from '../utils';
7 |
8 | export type DetailHeaderIconVisibility = {
9 | menuIcon: boolean;
10 | };
11 |
12 | export const useDetailHeader = (appType: AppType) => {
13 | const { pageType } = usePageType();
14 | const [title, setTitle] = useState('');
15 | const [iconVisibility, setIconVisibility] =
16 | useState({ menuIcon: false });
17 |
18 | useEffect(() => {
19 | if (pageType) {
20 | setTitle(getHeaderTitle(pageType));
21 |
22 | if (
23 | appType === APP_TYPE.SHELTER_APP &&
24 | (pageType === PAGE_TYPE.VOLUNTEERS_DETAIL ||
25 | pageType === PAGE_TYPE.ANIMALS_DETAIL)
26 | ) {
27 | setIconVisibility({ menuIcon: true });
28 | } else {
29 | setIconVisibility({ menuIcon: false });
30 | }
31 | }
32 | }, [appType, pageType]);
33 |
34 | return { title, iconVisibility };
35 | };
36 |
--------------------------------------------------------------------------------
/packages/layout/src/Header/index.tsx:
--------------------------------------------------------------------------------
1 | import { usePageType } from '@anifriends/hooks';
2 | import { AppType } from '@anifriends/types';
3 |
4 | import DefaultHeader from './DefaultHeader';
5 | import DetailHeader from './DetailHeader';
6 | import SearchHeader from './SearchHeader';
7 | import { useHeader } from './useHeader';
8 |
9 | const Headers = {
10 | DEFAULT: (props: HeaderProps) => ,
11 | DETAIL: (props: HeaderProps) => ,
12 | SEARCH: () => ,
13 | };
14 |
15 | export type HeaderProps = {
16 | appType: AppType;
17 | };
18 |
19 | export default function Header({ appType }: HeaderProps) {
20 | const { pageType } = usePageType();
21 | const { headerType } = useHeader();
22 |
23 | if (!pageType) {
24 | return null;
25 | }
26 |
27 | return Headers[headerType]({ appType });
28 | }
29 |
--------------------------------------------------------------------------------
/packages/layout/src/Header/useHeader.ts:
--------------------------------------------------------------------------------
1 | import { HEADER_TYPE } from '@anifriends/constants';
2 | import { usePageType } from '@anifriends/hooks';
3 | import { HeaderType } from '@anifriends/types';
4 | import { useEffect, useState } from 'react';
5 |
6 | import { getHeaderType } from './utils';
7 |
8 | export const useHeader = () => {
9 | const { pageType } = usePageType();
10 | const [headerType, setHeaderType] = useState(HEADER_TYPE.DEFAULT);
11 |
12 | useEffect(() => {
13 | if (pageType) {
14 | setHeaderType(getHeaderType(pageType));
15 | }
16 | }, [pageType]);
17 |
18 | return { headerType };
19 | };
20 |
--------------------------------------------------------------------------------
/packages/layout/src/Header/utils.ts:
--------------------------------------------------------------------------------
1 | import { HEADER_TYPE, headerTitle, PAGE_TYPE } from '@anifriends/constants';
2 | import { HeaderType, PageType } from '@anifriends/types';
3 |
4 | export const getHeaderType = (pageType: PageType): HeaderType => {
5 | if (
6 | pageType === PAGE_TYPE.VOLUNTEERS ||
7 | pageType === PAGE_TYPE.ANIMALS ||
8 | pageType === PAGE_TYPE.CHATTINGS ||
9 | pageType === PAGE_TYPE.MYPAGE
10 | ) {
11 | return HEADER_TYPE.DEFAULT;
12 | }
13 |
14 | if (
15 | pageType === PAGE_TYPE.VOLUNTEERS_SEARCH ||
16 | pageType === PAGE_TYPE.ANIMALS_SEARCH
17 | ) {
18 | return HEADER_TYPE.SEARCH;
19 | }
20 |
21 | return HEADER_TYPE.DETAIL;
22 | };
23 |
24 | export const getHeaderTitle = (pageType: PageType): string =>
25 | headerTitle[pageType];
26 |
--------------------------------------------------------------------------------
/packages/layout/svg.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.svg' {
2 | const content: string;
3 | export default content;
4 | }
5 |
--------------------------------------------------------------------------------
/packages/layout/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@anifriends/tsconfig/react-library.json",
3 | "include": ["src", "svg.d.ts"],
4 | "exclude": ["dist", "build", "node_modules"]
5 | }
6 |
--------------------------------------------------------------------------------
/packages/store/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@anifriends/store",
3 | "version": "0.0.0",
4 | "private": true,
5 | "license": "MIT",
6 | "main": "src/index.ts",
7 | "dependencies": {
8 | "zustand": "^4.4.4"
9 | },
10 | "devDependencies": {
11 | "@anifriends/tsconfig": "workspace:*",
12 | "typescript": "^5.0.2"
13 | },
14 | "publishConfig": {
15 | "access": "public"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/packages/store/src/authStore.ts:
--------------------------------------------------------------------------------
1 | import { create } from 'zustand';
2 |
3 | export type User = {
4 | accessToken: string;
5 | userId: number;
6 | };
7 |
8 | interface AuthState {
9 | user: User | null;
10 | }
11 |
12 | interface AuthActions {
13 | setUser: (user: User | null) => void;
14 | }
15 |
16 | export const useAuthStore = create((set) => ({
17 | user: null,
18 | setUser: (user: User | null) => {
19 | set({ user });
20 | },
21 | }));
22 |
--------------------------------------------------------------------------------
/packages/store/src/detailHeaderStore.ts:
--------------------------------------------------------------------------------
1 | import { create } from 'zustand';
2 |
3 | type DeleteFunction = (id: number) => void;
4 |
5 | interface DetailHeaderState {
6 | onDelete: DeleteFunction;
7 | }
8 |
9 | interface DetailHeaderActions {
10 | setOnDelete: (onDelete: DeleteFunction) => void;
11 | }
12 |
13 | export const useDetailHeaderStore = create<
14 | DetailHeaderState & DetailHeaderActions
15 | >((set) => ({
16 | onDelete: () => {},
17 | setOnDelete: (onDelete: DeleteFunction) => set(() => ({ onDelete })),
18 | }));
19 |
--------------------------------------------------------------------------------
/packages/store/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './authStore';
2 | export * from './detailHeaderStore';
3 | export * from './searchHeaderStore';
4 |
--------------------------------------------------------------------------------
/packages/store/src/searchHeaderStore.ts:
--------------------------------------------------------------------------------
1 | import { create } from 'zustand';
2 |
3 | type SearchFunction = (keyword: string) => void;
4 |
5 | interface SearchHeaderState {
6 | keyword: string;
7 | onSearch: SearchFunction;
8 | }
9 |
10 | interface SearchHeaderActions {
11 | setKeyword: (keyword: string) => void;
12 | setOnSearch: (onSearch: SearchFunction) => void;
13 | }
14 |
15 | export const useSearchHeaderStore = create<
16 | SearchHeaderState & SearchHeaderActions
17 | >((set) => ({
18 | keyword: '',
19 | onSearch: () => {},
20 | setKeyword: (keyword: string) => set(() => ({ keyword })),
21 | setOnSearch: (onSearch: SearchFunction) => set(() => ({ onSearch })),
22 | }));
23 |
--------------------------------------------------------------------------------
/packages/store/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@anifriends/tsconfig/base.json",
3 | "include": ["."],
4 | "exclude": ["dist", "build", "node_modules"],
5 | "compilerOptions": {
6 | "jsx": "react-jsx",
7 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
8 | "module": "ESNext",
9 | "target": "ES2020"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/packages/theme/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@anifriends/theme",
3 | "version": "0.0.0",
4 | "private": true,
5 | "license": "MIT",
6 | "main": "src/index.ts",
7 | "dependencies": {
8 | "@chakra-ui/react": "^2.8.1"
9 | },
10 | "devDependencies": {
11 | "@anifriends/tsconfig": "workspace:*"
12 | },
13 | "publishConfig": {
14 | "access": "public"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/packages/theme/src/index.ts:
--------------------------------------------------------------------------------
1 | import { extendTheme } from '@chakra-ui/react';
2 |
3 | const theme = extendTheme({
4 | fonts: {
5 | heading: `'IBMPlexSans'`,
6 | body: `'IBMPlexSans'`,
7 | },
8 | styles: {
9 | global: {
10 | body: {
11 | overscrollBehavior: 'none',
12 | },
13 | },
14 | },
15 | });
16 |
17 | export default theme;
18 |
--------------------------------------------------------------------------------
/packages/theme/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@anifriends/tsconfig/react-library.json",
3 | "include": ["src"],
4 | "exclude": ["dist", "build", "node_modules"]
5 | }
6 |
--------------------------------------------------------------------------------
/packages/types/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@anifriends/types",
3 | "version": "0.0.0",
4 | "private": true,
5 | "license": "MIT",
6 | "main": "src/index.ts",
7 | "devDependencies": {
8 | "@anifriends/tsconfig": "workspace:*",
9 | "typescript": "^5.0.2"
10 | },
11 | "publishConfig": {
12 | "access": "public"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/packages/types/src/apis/auth.ts:
--------------------------------------------------------------------------------
1 | export type CheckDuplicatedEmailRequestData = {
2 | email: string;
3 | };
4 |
5 | export type SigninRequestData = CheckDuplicatedEmailRequestData & {
6 | password: string;
7 | };
8 |
9 | export type ChangePasswordRequestData = {
10 | newPassword: string;
11 | oldPassword: string;
12 | };
13 |
14 | export type SigninResponseData = {
15 | accessToken: string;
16 | userId: number;
17 | role: string;
18 | };
19 |
20 | export type CheckDuplicatedEmailResponseData = {
21 | isDuplicated: boolean;
22 | };
23 |
--------------------------------------------------------------------------------
/packages/types/src/apis/error.ts:
--------------------------------------------------------------------------------
1 | export type ErrorResponseData = {
2 | errorCode: string;
3 | message: string;
4 | };
5 |
--------------------------------------------------------------------------------
/packages/types/src/apis/index.ts:
--------------------------------------------------------------------------------
1 | export * from './auth';
2 | export * from './error';
3 |
--------------------------------------------------------------------------------
/packages/types/src/app.ts:
--------------------------------------------------------------------------------
1 | export type AppType = 'SHELTER_APP' | 'VOLUNTEER_APP';
2 |
--------------------------------------------------------------------------------
/packages/types/src/gender.ts:
--------------------------------------------------------------------------------
1 | export type PersonGenderEng = 'FEMALE' | 'MALE';
2 |
3 | export type PersonGenderKor = '남성' | '여성';
4 |
--------------------------------------------------------------------------------
/packages/types/src/header.ts:
--------------------------------------------------------------------------------
1 | export type HeaderType = 'DEFAULT' | 'DETAIL' | 'SEARCH';
2 |
--------------------------------------------------------------------------------
/packages/types/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './apis';
2 | export * from './app';
3 | export * from './gender';
4 | export * from './header';
5 | export * from './page';
6 | export * from './period';
7 |
--------------------------------------------------------------------------------
/packages/types/src/page.ts:
--------------------------------------------------------------------------------
1 | export type PageType =
2 | | 'VOLUNTEERS'
3 | | 'VOLUNTEERS_DETAIL'
4 | | 'VOLUNTEERS_PROFILE'
5 | | 'VOLUNTEERS_SEARCH'
6 | | 'VOLUNTEERS_WRITE'
7 | | 'VOLUNTEERS_UPDATE'
8 | | 'ANIMALS'
9 | | 'ANIMALS_DETAIL'
10 | | 'ANIMALS_SEARCH'
11 | | 'ANIMALS_WRITE'
12 | | 'ANIMALS_UPDATE'
13 | | 'CHATTINGS'
14 | | 'CHATTINGS_ROOM'
15 | | 'MYPAGE'
16 | | 'MYPAGE_REVIEWS'
17 | | 'SETTINGS'
18 | | 'SETTINGS_ACCOUNT'
19 | | 'SETTINGS_PASSWORD'
20 | | 'MANAGE_ATTENDANCE'
21 | | 'MANAGE_APPLY'
22 | | 'NOTIFICATIONS'
23 | | 'SHELTERS_PROFILE'
24 | | 'SHELTERS_REVIEWS_WRITE'
25 | | 'SHELTERS_REVIEWS_UPDATE'
26 | | 'SIGNUP'
27 | | 'SIGNIN';
28 |
--------------------------------------------------------------------------------
/packages/types/src/period.ts:
--------------------------------------------------------------------------------
1 | export type Period =
2 | | 'WITHIN_ONE_DAY'
3 | | 'WITHIN_ONE_WEEK'
4 | | 'WITHIN_ONE_MONTH'
5 | | 'WITHIN_THREE_MONTH';
6 |
--------------------------------------------------------------------------------
/packages/types/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@anifriends/tsconfig/react-library.json",
3 | "include": ["src"],
4 | "exclude": ["dist", "build", "node_modules"]
5 | }
6 |
--------------------------------------------------------------------------------
/packages/utils/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@anifriends/utils",
3 | "version": "0.0.0",
4 | "private": true,
5 | "license": "MIT",
6 | "main": "src/index.ts",
7 | "dependencies": {
8 | "@anifriends/constants": "workspace:*",
9 | "@anifriends/types": "workspace:*",
10 | "@chakra-ui/react": "^2.8.1",
11 | "react-image-file-resizer": "^0.4.8",
12 | "zod": "^3.22.4"
13 | },
14 | "devDependencies": {
15 | "@anifriends/tsconfig": "workspace:*",
16 | "typescript": "^5.0.2"
17 | },
18 | "publishConfig": {
19 | "access": "public"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/packages/utils/src/date.ts:
--------------------------------------------------------------------------------
1 | import { MILISECONDS, WEEK_DAYS } from '@anifriends/constants';
2 |
3 | export const createFormattedTime = (
4 | date: Date,
5 | format = 'YYYY.MM.DD',
6 | ): string => {
7 | const formatReplacements: Record = {
8 | YYYY: String(date.getFullYear()),
9 | YY: String(date.getFullYear()).substring(2, 4),
10 | MM: String(date.getMonth() + 1).padStart(2, '0'),
11 | DD: String(date.getDate()).padStart(2, '0'),
12 | hh: String(date.getHours()).padStart(2, '0'),
13 | mm: String(date.getMinutes()).padStart(2, '0'),
14 | };
15 |
16 | const formattedTime = format.replace(
17 | /YYYY|YY|MM|DD|hh|mm/g,
18 | (match) => formatReplacements[match],
19 | );
20 |
21 | return formattedTime;
22 | };
23 |
24 | export const createWeekDayLocalString = (date: Date) => {
25 | return WEEK_DAYS[date.getDay()];
26 | };
27 |
28 | export const isSameDay = (a: Date, b: Date): boolean => {
29 | const diff = (a.getTime() - b.getTime()) / 1000;
30 |
31 | return Math.trunc(diff / MILISECONDS.DAY) === 0;
32 | };
33 |
34 | export const getDDay = (deadLine: string): number => {
35 | const deadLineDate = new Date(deadLine).getTime();
36 | const currentDate = new Date().getTime();
37 | const diffDate = deadLineDate - currentDate;
38 |
39 | return Math.floor(diffDate / (1000 * MILISECONDS.DAY));
40 | };
41 |
42 | export const getAge = (birthDate: string) => {
43 | const currentDate = new Date();
44 | const parsedBirthDate = new Date(birthDate);
45 | const age = currentDate.getFullYear() - parsedBirthDate.getFullYear() - 1;
46 | const isPassed =
47 | currentDate.getMonth() < parsedBirthDate.getMonth() ||
48 | (currentDate.getMonth() === parsedBirthDate.getMonth() &&
49 | currentDate.getDate() <= parsedBirthDate.getDate());
50 |
51 | return age + Number(isPassed);
52 | };
53 |
54 | export const getKoreanTime = (date: Date) => {
55 | date.setHours(date.getHours() + 9);
56 | return date;
57 | };
58 |
--------------------------------------------------------------------------------
/packages/utils/src/errorMessage.ts:
--------------------------------------------------------------------------------
1 | export const getErrorMessage = (status: number) => {
2 | switch (status) {
3 | case 401:
4 | return {
5 | title: '로그인이 필요한 서비스입니다',
6 | content: '로그인을 해주세요',
7 | };
8 | case 403:
9 | return {
10 | title: '접근 권한이 없습니다',
11 | content: '홈으로 이동해주세요',
12 | };
13 | case 409:
14 | case 500:
15 | default:
16 | return {
17 | title: '잠시 연결이 늦어지고 있습니다',
18 | content: '다시 한번 시도해 주세요',
19 | };
20 | }
21 | };
22 |
--------------------------------------------------------------------------------
/packages/utils/src/image.ts:
--------------------------------------------------------------------------------
1 | import Resizer from 'react-image-file-resizer';
2 |
3 | const resize = (imageFile: File): Promise => {
4 | return new Promise((resolve) => {
5 | Resizer.imageFileResizer(
6 | imageFile,
7 | 1000,
8 | 1000,
9 | 'WEBP',
10 | 100,
11 | 0,
12 | (uri) => resolve(uri as File),
13 | 'file',
14 | );
15 | });
16 | };
17 |
18 | export const resizeImageFile = async (
19 | imageFile: File,
20 | maxSizeMB: number,
21 | ): Promise => {
22 | if (imageFile.size <= maxSizeMB * 1024 ** 2) {
23 | return imageFile;
24 | }
25 |
26 | const resizedImageFile = await resize(imageFile);
27 | return resizeImageFile(resizedImageFile, maxSizeMB);
28 | };
29 |
--------------------------------------------------------------------------------
/packages/utils/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './date';
2 | export * from './errorMessage';
3 | export * from './image';
4 | export * from './localStorage';
5 | export * from './period';
6 | export * from './toast';
7 | export * from './validations';
8 |
--------------------------------------------------------------------------------
/packages/utils/src/localStorage.ts:
--------------------------------------------------------------------------------
1 | export const setItemToStorage = (key: string, value: string) => {
2 | localStorage.setItem(key, value);
3 | };
4 |
5 | export const getItemFromStorage = (key: string) => {
6 | const value = localStorage.getItem(key);
7 |
8 | if (!value) {
9 | throw new Error('[Error] : 로컬 스토리지에 유저 정보가 없습니다');
10 | }
11 |
12 | return value;
13 | };
14 |
15 | export const removeItemFromStorage = (key: string) => {
16 | localStorage.removeItem(key);
17 | };
18 |
--------------------------------------------------------------------------------
/packages/utils/src/period.ts:
--------------------------------------------------------------------------------
1 | import type { Period } from '@anifriends/types';
2 |
3 | import { createFormattedTime } from './date';
4 |
5 | const periodEndDate: Record number> = {
6 | WITHIN_ONE_DAY: (date: Date) => date.getDate() + 1,
7 | WITHIN_ONE_WEEK: (date: Date) => date.getDate() + 7,
8 | WITHIN_ONE_MONTH: (date: Date) => date.getMonth() + 1,
9 | WITHIN_THREE_MONTH: (date: Date) => date.getMonth() + 3,
10 | };
11 |
12 | export const getDatesFromPeriod = (period?: Period) => {
13 | if (!period) {
14 | return {
15 | startDate: undefined,
16 | endDate: undefined,
17 | };
18 | }
19 |
20 | const startDate = new Date();
21 | const endDate = new Date();
22 |
23 | endDate.setDate(periodEndDate[period](startDate));
24 |
25 | return {
26 | startDate: createFormattedTime(startDate, 'YYYY-MM-DD'),
27 | endDate: createFormattedTime(endDate, 'YYYY-MM-DD'),
28 | };
29 | };
30 |
--------------------------------------------------------------------------------
/packages/utils/src/toast.ts:
--------------------------------------------------------------------------------
1 | import { CreateToastFnReturn, UseToastOptions } from '@chakra-ui/react';
2 |
3 | export const updateToast = ({
4 | toast,
5 | toastId,
6 | toastOptions,
7 | }: {
8 | toast: CreateToastFnReturn;
9 | toastId: string;
10 | toastOptions: UseToastOptions;
11 | }) => {
12 | const updateToastOptions = {
13 | ...toastOptions,
14 | id: toastId,
15 | };
16 |
17 | if (!toast.isActive(toastId)) {
18 | toast(updateToastOptions);
19 | } else {
20 | toast.update(toastId, updateToastOptions);
21 | }
22 | };
23 |
--------------------------------------------------------------------------------
/packages/utils/src/validations.ts:
--------------------------------------------------------------------------------
1 | import * as z from 'zod';
2 |
3 | import { createFormattedTime } from './date';
4 |
5 | export const email = z
6 | .string()
7 | .min(1, '이메일은 필수 정보입니다')
8 | .regex(
9 | /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
10 | '이메일 형식이 올바르지 않습니다',
11 | );
12 | export const isEmailDuplicated = z.boolean();
13 | export const password = z.string().min(1, '비밀번호 정보는 필수입니다');
14 | // TODO 나중에 추가 예정
15 | //
16 | // .regex(
17 | // /^(?=.*[!@#$%^&*()\-_=+[\]\\|{};:'",<.>/?]+)(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}$/,
18 | // '비밀번호는 필수 정보입니다(8자 이상)',
19 | // ),
20 | export const passwordConfirm = z
21 | .string()
22 | .min(1, '비밀번호 확인 정보는 필수입니다');
23 | export const oldPassword = z.string().min(1, '기존 비밀번호 정보는 필수입니다');
24 | export const newPassword = z.string().min(1, '변경 비밀번호 정보는 필수입니다');
25 | // TODO 나중에 추가 예정
26 | //
27 | // .regex(
28 | // /^(?=.*[!@#$%^&*()\-_=+[\]\\|{};:'",<.>/?]+)(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}$/,
29 | // '비밀번호는 필수 정보입니다(8자 이상)',
30 | // ),
31 | export const newPasswordConfirm = z
32 | .string()
33 | .min(1, '변경 비밀번호 확인 정보는 필수입니다');
34 | export const name = z.string().min(1, '보호소 이름 정보는 필수입니다');
35 | export const address = z.string().min(1, '보호소 주소 정보는 필수입니다');
36 | export const addressDetail = z
37 | .string()
38 | .min(1, '보호소 상세주소 정보는 필수입니다');
39 | export const isOpenedAddress = z.boolean();
40 | export const phoneNumber = z
41 | .string()
42 | .min(1, '보호소 전화번호 정보는 필수입니다')
43 | .regex(/^\d{2,3}-\d{3,4}-\d{4}$/, '전화번호 형식이 올바르지 않습니다');
44 | export const sparePhoneNumber = z.union([
45 | z.literal(''),
46 | z
47 | .string()
48 | .regex(/^\d{2,3}-\d{3,4}-\d{4}$/, '전화번호 형식이 올바르지 않습니다'),
49 | ]);
50 | export const gender = z.enum(['FEMALE', 'MALE']);
51 | export const birthDate = z
52 | .string()
53 | .min(1, '생년월일 정보는 필수입니다')
54 | .refine(
55 | (val) => new Date(val) < new Date(),
56 | `${createFormattedTime(new Date())} 이전으로 선택해주세요`,
57 | );
58 |
--------------------------------------------------------------------------------
/packages/utils/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@anifriends/tsconfig/react-library.json",
3 | "include": ["src"],
4 | "exclude": ["dist", "build", "node_modules"]
5 | }
6 |
--------------------------------------------------------------------------------
/packlint.config.mjs:
--------------------------------------------------------------------------------
1 | export default {
2 | files: [
3 | './package.json',
4 | './packages/*/package.json',
5 | './apps/*/package.json',
6 | ],
7 | };
8 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - 'apps/*'
3 | - 'configs/*'
4 | - 'packages/*'
5 |
--------------------------------------------------------------------------------
/turbo.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://turbo.build/schema.json",
3 | "pipeline": {
4 | "build": {
5 | "dependsOn": ["^build"],
6 | "outputs": ["dist/**"]
7 | },
8 | "lint": {},
9 | "dev": {
10 | "cache": false,
11 | "persistent": true
12 | },
13 | "test": {},
14 | "test:watch": {
15 | "cache": false
16 | }
17 | },
18 | "globalDotEnv": [".env"]
19 | }
20 |
--------------------------------------------------------------------------------