├── .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 | 13 | 17 | 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 | 13 | 17 | 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 | 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 | 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 | 10 | 11 | 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 | 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 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/assets/bottomNavBar/icon_mypage_unselected.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/assets/bottomNavBar/icon_volunteers_selected.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/assets/bottomNavBar/icon_volunteers_unselected.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/assets/icon-IoEyeSharp.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/assets/icon_BiX.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/assets/icon_IoCamera.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/assets/icon_applicant.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/assets/icon_back.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/assets/icon_menu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/assets/icon_next.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/assets/icon_notifications.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/assets/icon_review_next.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/assets/icon_search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 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 | 13 | 14 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 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 | 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 | 19 | 20 | Menu Icon 21 | 22 | {children} 23 | 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 | 13 | 14 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /packages/icons/src/BackIcon.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps } from 'react'; 2 | 3 | export function BackIcon(props: ComponentProps<'svg'>) { 4 | return ( 5 | 13 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /packages/icons/src/BiXIcon.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps } from 'react'; 2 | 3 | export function BiXIcon(props: ComponentProps<'svg'>) { 4 | return ( 5 | 13 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /packages/icons/src/IoCameraIcon.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps } from 'react'; 2 | 3 | export function IoCameraIcon(props: Omit, 'children'>) { 4 | return ( 5 | 13 | 17 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /packages/icons/src/IoEyeSharp.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps } from 'react'; 2 | 3 | export function IoEyeSharp({ ...props }: ComponentProps<'svg'>) { 4 | return ( 5 | 13 | 17 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /packages/icons/src/MenuIcon.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps } from 'react'; 2 | 3 | export function MenuIcon(props: ComponentProps<'svg'>) { 4 | return ( 5 | 13 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /packages/icons/src/NextIcon.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps } from 'react'; 2 | 3 | export function NextIcon(props: ComponentProps<'svg'>) { 4 | return ( 5 | 13 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /packages/icons/src/NotificationIcon.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps } from 'react'; 2 | 3 | export function NotificationIcon(props: ComponentProps<'svg'>) { 4 | return ( 5 | 13 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /packages/icons/src/ReviewNextIcon.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps } from 'react'; 2 | 3 | export function ReviewNextIcon(props: ComponentProps<'svg'>) { 4 | return ( 5 | 13 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /packages/icons/src/SearchIcon.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps } from 'react'; 2 | 3 | export function SearchIcon(props: ComponentProps<'svg'>) { 4 | return ( 5 | 13 | 17 | 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 | Back Icon 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 | --------------------------------------------------------------------------------