├── src ├── shared │ ├── components │ │ ├── chip │ │ │ ├── index.ts │ │ │ ├── Chip.stories.tsx │ │ │ └── Chip.tsx │ │ ├── button │ │ │ ├── index.ts │ │ │ ├── Button.stories.tsx │ │ │ └── Button.tsx │ │ ├── input │ │ │ ├── index.ts │ │ │ └── Input.tsx │ │ ├── label │ │ │ ├── index.ts │ │ │ └── Label.tsx │ │ ├── sheet │ │ │ └── index.ts │ │ ├── toggle │ │ │ ├── index.ts │ │ │ └── Toggle.tsx │ │ ├── textarea │ │ │ ├── index.ts │ │ │ └── Textarea.tsx │ │ ├── text-input │ │ │ ├── index.ts │ │ │ ├── TextInput.stories.tsx │ │ │ └── TextInput.tsx │ │ ├── modal │ │ │ └── index.ts │ │ ├── layout │ │ │ ├── index.ts │ │ │ ├── BottomNavLayout.module.css │ │ │ ├── BottomNavLayout.tsx │ │ │ ├── header │ │ │ │ └── ProfileHeader.tsx │ │ │ └── GlobalLayout.tsx │ │ ├── navigation │ │ │ └── index.ts │ │ ├── spacing │ │ │ └── index.tsx │ │ ├── toast │ │ │ ├── index.ts │ │ │ ├── ToastContainer.tsx │ │ │ └── Toast.tsx │ │ ├── loading │ │ │ ├── index.ts │ │ │ ├── ComponentLoading.tsx │ │ │ └── PageLoading.tsx │ │ ├── index.ts │ │ ├── challenge │ │ │ ├── ChallengeNotFound.tsx │ │ │ ├── Rules.tsx │ │ │ └── ChallengeParticipants.tsx │ │ ├── date-chip │ │ │ └── index.tsx │ │ ├── font │ │ │ └── ApplyingFont.tsx │ │ ├── challenge-list │ │ │ └── index.tsx │ │ ├── form │ │ │ └── schema.ts │ │ ├── progress-bar │ │ │ └── index.tsx │ │ ├── kakao-login │ │ │ └── index.tsx │ │ ├── comment │ │ │ ├── CommentContainer.tsx │ │ │ └── Comment.tsx │ │ ├── challenge-achievement │ │ │ ├── ChallengeAchievementContainer.tsx │ │ │ └── ChallengeAchievement.tsx │ │ ├── challenge-category │ │ │ ├── ChallengeCategory.tsx │ │ │ └── ChallengeCategories.tsx │ │ ├── bottom-sheet │ │ │ ├── RecordBottomSheet.tsx │ │ │ ├── ChallengeFilter.Sheet.tsx │ │ │ └── ChallengeLeaveSheet.tsx │ │ ├── error-page │ │ │ └── index.tsx │ │ └── tooltip │ │ │ └── index.tsx │ ├── constants │ │ ├── index.ts │ │ ├── urls.ts │ │ ├── spending.ts │ │ └── challenge.ts │ ├── types │ │ ├── image.ts │ │ ├── presigned.ts │ │ ├── api.ts │ │ ├── emoji.ts │ │ ├── layout.ts │ │ ├── spending.ts │ │ ├── comment.ts │ │ ├── challenge.ts │ │ └── user.ts │ ├── hooks │ │ ├── index.ts │ │ ├── useScrollToBottom.ts │ │ ├── useClickOutside.ts │ │ ├── useToast.ts │ │ ├── useKeepScrollPosition.ts │ │ ├── useChallengeAchievement.ts │ │ └── useIntersectionObserver.ts │ ├── utils │ │ ├── file.ts │ │ ├── emoji.ts │ │ ├── currency.ts │ │ ├── time │ │ │ ├── time.spec.ts │ │ │ └── time.ts │ │ └── date │ │ │ ├── date.spec.ts │ │ │ └── date.ts │ ├── store │ │ ├── user.ts │ │ ├── room.ts │ │ └── toast.ts │ └── hof │ │ └── withAuth.ts ├── pages │ ├── index.tsx │ ├── jaringobi │ │ └── index.tsx │ ├── not-found │ │ └── index.tsx │ ├── internal-server-error │ │ └── index.tsx │ ├── _document.tsx │ ├── auth │ │ ├── kakao │ │ │ └── index.tsx │ │ └── login │ │ │ └── index.tsx │ ├── _app.tsx │ ├── add-spending │ │ └── index.tsx │ ├── my-poor-room │ │ └── index.tsx │ ├── my-page │ │ └── index.tsx │ └── search │ │ └── index.tsx ├── features │ ├── emoji │ │ ├── index.ts │ │ ├── queries.ts │ │ └── Emoji.tsx │ ├── feed │ │ ├── index.ts │ │ ├── FeedCreationDate.tsx │ │ ├── FeedUserImg.tsx │ │ ├── FeedDate.tsx │ │ ├── FeedUserInfo.tsx │ │ ├── RecrutingChallenge.tsx │ │ ├── NoChallengeAvailable.tsx │ │ ├── queries.ts │ │ └── Feed.tsx │ ├── spending │ │ └── queries.ts │ ├── challenge │ │ ├── queryKey.ts │ │ └── queries.ts │ ├── comment │ │ └── queries.ts │ └── profile │ │ ├── Profile.tsx │ │ ├── queries.ts │ │ └── Board.tsx ├── lib │ ├── utils.ts │ ├── validation │ │ └── user.ts │ └── interfaces │ │ └── index.ts ├── mocks │ ├── browser.ts │ ├── server.ts │ ├── index.ts │ └── handlers │ │ ├── handlers.ts │ │ ├── emoji.ts │ │ ├── comment.ts │ │ ├── challenge.ts │ │ └── user.ts ├── service │ ├── spending.ts │ ├── auth-refresh.ts │ ├── image.ts │ ├── comment.ts │ ├── emoji.ts │ ├── challenge.ts │ ├── feed.ts │ ├── index.ts │ ├── user.ts │ ├── index.spec.ts │ └── auth.ts └── styles │ ├── globals.css │ └── tailwind │ ├── color.js │ └── typography.js ├── .husky ├── pre-commit ├── pre-push ├── commit-msg └── prepare-commit-msg ├── jest.setup.js ├── public ├── images │ ├── check.png │ ├── fish.png │ ├── high.png │ ├── low.png │ ├── mark.png │ ├── 떡볶이.jpg │ ├── baemin.png │ ├── camera.png │ ├── login.webp │ ├── medium.png │ ├── profile.png │ ├── high-check.png │ ├── jalingobi.png │ ├── low-check.png │ ├── medium-check.png │ ├── fish-with-tear.png │ └── jalingobi-og.webp ├── fonts │ ├── Pretendard-Medium.woff2 │ └── Pretendard-Regular.woff2 └── svgs │ ├── icon-circle.svg │ ├── icon-timer.svg │ ├── icon-polygon-up.svg │ ├── icon-polygon-down.svg │ ├── icon-add.svg │ ├── icon-add copy.svg │ ├── icon-chevron-right.svg │ ├── icon-chevron-up.svg │ ├── icon-arrow-right.svg │ ├── icon-chevron-down.svg │ ├── icon-cancel.svg │ ├── icon-arrow-up-fill.svg │ ├── icon-loading.svg │ ├── icon-user.svg │ ├── icon-chevron-left.svg │ ├── icon-add-circle.svg │ ├── icon-reaction.svg │ ├── icon-note.svg │ ├── icon-x.svg │ ├── icon-check.svg │ ├── icon-add-square.svg │ ├── icon-search.svg │ ├── icon-comment-big.svg │ ├── icon-clock.svg │ ├── icon-overflow.svg │ ├── icon-chat.svg │ ├── icon-tile.svg │ ├── icon-welldone-small.svg │ ├── icon-regretful-small.svg │ ├── icon-crazy-small.svg │ ├── icon-comment-small.svg │ ├── filter.svg │ ├── icon-filter.svg │ ├── icon-clothes.svg │ ├── icon-selected-clothes.svg │ ├── icon-rice.svg │ ├── icon-selected-rice.svg │ ├── icon-settings.svg │ ├── icon-car.svg │ ├── icon-welldone-big.svg │ ├── icon-regretful-big.svg │ ├── icon-crazy-big.svg │ ├── icon-taxi.svg │ ├── icon-selected-taxi.svg │ ├── icon-hobby.svg │ ├── icon-selected-hobby.svg │ └── index.ts ├── postcss.config.js ├── .dockerignore ├── commitlint.config.js ├── global.d.ts ├── .editorconfig ├── .prettierrc ├── makefile ├── .storybook ├── preview.ts └── main.ts ├── .commitlintrc.js ├── tsconfig.json ├── .github └── workflows │ ├── chromatic.yml │ └── deployToECR.yaml ├── jest.config.mjs ├── dockerfile ├── tailwind.config.js └── next.config.js /src/shared/components/chip/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Chip'; 2 | -------------------------------------------------------------------------------- /src/shared/components/button/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Button'; 2 | -------------------------------------------------------------------------------- /src/shared/components/input/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Input'; 2 | -------------------------------------------------------------------------------- /src/shared/components/label/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Label'; 2 | -------------------------------------------------------------------------------- /src/shared/components/sheet/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Sheet'; 2 | -------------------------------------------------------------------------------- /src/shared/components/toggle/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Toggle'; 2 | -------------------------------------------------------------------------------- /src/shared/components/textarea/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Textarea'; 2 | -------------------------------------------------------------------------------- /src/shared/constants/index.ts: -------------------------------------------------------------------------------- 1 | export const CHALLENGE_ID_MY_ROOM = 0; 2 | -------------------------------------------------------------------------------- /src/shared/components/text-input/index.ts: -------------------------------------------------------------------------------- 1 | export * from './TextInput'; 2 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | export default function Home() { 2 | return
; 3 | } 4 | -------------------------------------------------------------------------------- /src/shared/constants/urls.ts: -------------------------------------------------------------------------------- 1 | export const URL = { 2 | SEARCH: '/search', 3 | }; 4 | -------------------------------------------------------------------------------- /src/features/emoji/index.ts: -------------------------------------------------------------------------------- 1 | import { Emoji } from './Emoji'; 2 | 3 | export { Emoji }; 4 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | pnpx lint-staged 5 | -------------------------------------------------------------------------------- /src/shared/components/modal/index.ts: -------------------------------------------------------------------------------- 1 | import { Modal } from './Modal'; 2 | 3 | export { Modal }; 4 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | import '@testing-library/jest-dom/extend-expect'; 3 | -------------------------------------------------------------------------------- /public/images/check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/jalingobi-client/HEAD/public/images/check.png -------------------------------------------------------------------------------- /public/images/fish.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/jalingobi-client/HEAD/public/images/fish.png -------------------------------------------------------------------------------- /public/images/high.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/jalingobi-client/HEAD/public/images/high.png -------------------------------------------------------------------------------- /public/images/low.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/jalingobi-client/HEAD/public/images/low.png -------------------------------------------------------------------------------- /public/images/mark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/jalingobi-client/HEAD/public/images/mark.png -------------------------------------------------------------------------------- /public/images/떡볶이.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/jalingobi-client/HEAD/public/images/떡볶이.jpg -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | protected_branch='main' 5 | 6 | -------------------------------------------------------------------------------- /public/images/baemin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/jalingobi-client/HEAD/public/images/baemin.png -------------------------------------------------------------------------------- /public/images/camera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/jalingobi-client/HEAD/public/images/camera.png -------------------------------------------------------------------------------- /public/images/login.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/jalingobi-client/HEAD/public/images/login.webp -------------------------------------------------------------------------------- /public/images/medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/jalingobi-client/HEAD/public/images/medium.png -------------------------------------------------------------------------------- /public/images/profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/jalingobi-client/HEAD/public/images/profile.png -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | pnpm commitlint --edit "${1}" 5 | -------------------------------------------------------------------------------- /public/images/high-check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/jalingobi-client/HEAD/public/images/high-check.png -------------------------------------------------------------------------------- /public/images/jalingobi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/jalingobi-client/HEAD/public/images/jalingobi.png -------------------------------------------------------------------------------- /public/images/low-check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/jalingobi-client/HEAD/public/images/low-check.png -------------------------------------------------------------------------------- /src/shared/components/layout/index.ts: -------------------------------------------------------------------------------- 1 | import GlobalLayout from './GlobalLayout'; 2 | 3 | export { GlobalLayout }; 4 | -------------------------------------------------------------------------------- /src/shared/constants/spending.ts: -------------------------------------------------------------------------------- 1 | export const contentMaxLength = 80; 2 | 3 | export const maxPrice = 1000000; 4 | -------------------------------------------------------------------------------- /public/images/medium-check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/jalingobi-client/HEAD/public/images/medium-check.png -------------------------------------------------------------------------------- /src/shared/types/image.ts: -------------------------------------------------------------------------------- 1 | export type PresignedUrlResponse = { 2 | presignedUrl: string; 3 | imgUrl: string; 4 | }; 5 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/images/fish-with-tear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/jalingobi-client/HEAD/public/images/fish-with-tear.png -------------------------------------------------------------------------------- /public/images/jalingobi-og.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/jalingobi-client/HEAD/public/images/jalingobi-og.webp -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | dockerfile 2 | dockerfile.* 3 | .dockerignore 4 | node_modules 5 | npm-debug.log 6 | README.md 7 | .next 8 | .git 9 | -------------------------------------------------------------------------------- /public/fonts/Pretendard-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/jalingobi-client/HEAD/public/fonts/Pretendard-Medium.woff2 -------------------------------------------------------------------------------- /public/fonts/Pretendard-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/jalingobi-client/HEAD/public/fonts/Pretendard-Regular.woff2 -------------------------------------------------------------------------------- /src/shared/components/navigation/index.ts: -------------------------------------------------------------------------------- 1 | import BottomNavigation from './BottomNavigation'; 2 | 3 | export { BottomNavigation }; 4 | -------------------------------------------------------------------------------- /src/shared/components/layout/BottomNavLayout.module.css: -------------------------------------------------------------------------------- 1 | .layout { 2 | height: 100dvh; 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | -------------------------------------------------------------------------------- /src/shared/types/presigned.ts: -------------------------------------------------------------------------------- 1 | export type PresignedImageType = 2 | | 'RECORD' 3 | | 'CHALLENGE' 4 | | 'PROFILE' 5 | | 'CUSTOM_PROFILE'; 6 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | // https://commitlint.js.org/#/reference-rules 2 | module.exports = { 3 | extends: ['@commitlint/config-conventional'], 4 | }; 5 | -------------------------------------------------------------------------------- /src/shared/types/api.ts: -------------------------------------------------------------------------------- 1 | export type ApiResponse = { 2 | isSuccess: boolean; 3 | code: number; 4 | message: string; 5 | result: T; 6 | }; 7 | -------------------------------------------------------------------------------- /src/shared/types/emoji.ts: -------------------------------------------------------------------------------- 1 | import { EmojiType } from './feed'; 2 | 3 | export type EmojiRequest = { 4 | recordId: number; 5 | type: EmojiType; 6 | }; 7 | -------------------------------------------------------------------------------- /global.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' { 2 | import React from 'react'; 3 | 4 | const svg: React.FC>; 5 | export default svg; 6 | } 7 | -------------------------------------------------------------------------------- /src/shared/components/spacing/index.tsx: -------------------------------------------------------------------------------- 1 | export default function Spacing({ height }: { height: number }) { 2 | return
; 3 | } 4 | -------------------------------------------------------------------------------- /src/shared/components/toast/index.ts: -------------------------------------------------------------------------------- 1 | import { Toast } from './Toast'; 2 | import { ToastsContainer } from './ToastContainer'; 3 | 4 | export { Toast, ToastsContainer }; 5 | -------------------------------------------------------------------------------- /.husky/prepare-commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | CURRENT_SCOPE=$(git branch --show-current | cut -d '/' -f 1) 5 | COMMIT_MESSAGE=$(cat "$1") 6 | 7 | -------------------------------------------------------------------------------- /public/svgs/icon-circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/svgs/icon-timer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/shared/components/loading/index.ts: -------------------------------------------------------------------------------- 1 | import { ComponentLoading } from './ComponentLoading'; 2 | import { PageLoading } from './PageLoading'; 3 | 4 | export { ComponentLoading, PageLoading }; 5 | -------------------------------------------------------------------------------- /public/svgs/icon-polygon-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from 'clsx'; 2 | import { twMerge } from 'tailwind-merge'; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /src/shared/components/index.ts: -------------------------------------------------------------------------------- 1 | import { BottomSheet } from './bottom-sheet'; 2 | import Spacing from './spacing'; 3 | import { Tooltip } from './tooltip'; 4 | 5 | export { Tooltip, Spacing, BottomSheet }; 6 | -------------------------------------------------------------------------------- /public/svgs/icon-polygon-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/shared/hooks/index.ts: -------------------------------------------------------------------------------- 1 | import useIntersectionObserver from './useIntersectionObserver'; 2 | import { useScrollToBottom } from './useScrollToBottom'; 3 | 4 | export { useIntersectionObserver, useScrollToBottom }; 5 | -------------------------------------------------------------------------------- /src/shared/utils/file.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 확장자를 반환합니다. 3 | * @example 4 | * @param "image/png" 5 | * @return "png" 6 | */ 7 | export const getExtension = (type: string) => { 8 | return type.split('/')[1]; 9 | }; 10 | -------------------------------------------------------------------------------- /public/svgs/icon-add.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/svgs/icon-add copy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/jaringobi/index.tsx: -------------------------------------------------------------------------------- 1 | import { BottomNavigation } from '@/shared/components/navigation'; 2 | 3 | export default function Jaringobi() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /src/features/feed/index.ts: -------------------------------------------------------------------------------- 1 | import { ChallengeRoomFeedList } from './ChallengeRoomFeedList'; 2 | import { Feed } from './Feed'; 3 | import { MyRoomFeedList } from './MyRoomFeedList'; 4 | 5 | export { Feed, MyRoomFeedList, ChallengeRoomFeedList }; 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # https://github.com/editorconfig/editorconfig/wiki/EditorConfig-Properties 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | max_line_length = 80 11 | -------------------------------------------------------------------------------- /public/svgs/icon-chevron-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/mocks/browser.ts: -------------------------------------------------------------------------------- 1 | // src/mocks/browser.js 2 | import { setupWorker } from 'msw'; 3 | 4 | import { handlers } from '@/mocks/handlers/handlers'; 5 | 6 | // This configures a Service Worker with the given request handlers. 7 | export const worker = setupWorker(...handlers); 8 | -------------------------------------------------------------------------------- /public/svgs/icon-chevron-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/features/spending/queries.ts: -------------------------------------------------------------------------------- 1 | import { useMutation } from '@tanstack/react-query'; 2 | 3 | import { addSpending } from '@/service/spending'; 4 | 5 | export const useAddSpendingMutation = () => { 6 | return useMutation({ 7 | mutationFn: addSpending, 8 | }); 9 | }; 10 | -------------------------------------------------------------------------------- /src/mocks/server.ts: -------------------------------------------------------------------------------- 1 | // src/mocks/server.js 2 | import { setupServer } from 'msw/node'; 3 | 4 | import { handlers } from '@/mocks/handlers/handlers'; 5 | 6 | // This configures a request mocking server with the given request handlers. 7 | export const server = setupServer(...handlers); 8 | -------------------------------------------------------------------------------- /public/svgs/icon-arrow-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/svgs/icon-chevron-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/lib/validation/user.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const profileSchema = z.object({ 4 | nickName: z 5 | .string() 6 | .min(1, { 7 | message: '유저 이름은 1글자 이상이어야 합니다.', 8 | }) 9 | .max(16, { 10 | message: '유저 이름은 16글자를 초과할 수 없습니다.', 11 | }), 12 | }); 13 | -------------------------------------------------------------------------------- /src/shared/types/layout.ts: -------------------------------------------------------------------------------- 1 | import { NextPage } from 'next'; 2 | import { ReactElement, ReactNode } from 'react'; 3 | 4 | // eslint-disable-next-line @typescript-eslint/ban-types 5 | export type NextPageWithLayout

= NextPage & { 6 | getLayout?: (page: ReactElement) => ReactNode; 7 | }; 8 | -------------------------------------------------------------------------------- /src/shared/utils/emoji.ts: -------------------------------------------------------------------------------- 1 | import { EmojiType } from '@/shared/types/feed'; 2 | 3 | export function createEmojiInfo( 4 | type: EmojiType, 5 | count: number, 6 | selected: EmojiType | null, 7 | ) { 8 | return { 9 | type, 10 | count, 11 | selected: selected === type, 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /src/mocks/index.ts: -------------------------------------------------------------------------------- 1 | async function initMocks() { 2 | if (typeof window === 'undefined') { 3 | const { server } = await import('./server'); 4 | server.listen(); 5 | } else { 6 | const { worker } = await import('./browser'); 7 | worker.start(); 8 | } 9 | } 10 | 11 | initMocks(); 12 | 13 | export {}; 14 | -------------------------------------------------------------------------------- /public/svgs/icon-cancel.svg: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /src/shared/components/loading/ComponentLoading.tsx: -------------------------------------------------------------------------------- 1 | import { IconLoading } from '@/public/svgs'; 2 | 3 | export const ComponentLoading = () => { 4 | return ( 5 |

6 | 7 |
8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/shared/types/spending.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | import { spendSchema } from '../components/form/schema'; 4 | 5 | import { ChallengeEvaluation } from './challenge'; 6 | 7 | export type Spending = { 8 | challengeId: number; 9 | imageUrl?: string; 10 | evaluation?: ChallengeEvaluation; 11 | } & z.infer; 12 | -------------------------------------------------------------------------------- /src/features/feed/FeedCreationDate.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils'; 2 | 3 | type FeedCreationDateProps = { date: string; className?: string }; 4 | 5 | export const FeedCreationDate = ({ 6 | date, 7 | className, 8 | }: FeedCreationDateProps) => ( 9 |

{date}

10 | ); 11 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true, 6 | "trailingComma": "all", 7 | "arrowParens": "always", 8 | "bracketSpacing": true, 9 | "jsxBracketSameLine": false, 10 | "plugins": [ 11 | "prettier-plugin-tailwindcss" 12 | ], 13 | "tailwindFunctions": ["cva", "clsx"] 14 | } 15 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | DOCKER_IMAGE_NAME=jaringobi 2 | DOCKER_IMAGE_TAG=latest 3 | 4 | docker-build: 5 | docker build . -t $(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG) 6 | 7 | docker-rebuild: 8 | docker build . -t $(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG) --no-cache 9 | 10 | docker-run: 11 | docker run -d -it --rm -p 3000:3000 $(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG) 12 | 13 | -------------------------------------------------------------------------------- /src/shared/components/loading/PageLoading.tsx: -------------------------------------------------------------------------------- 1 | import { IconLoading } from '@/public/svgs'; 2 | 3 | export const PageLoading = () => { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/shared/constants/challenge.ts: -------------------------------------------------------------------------------- 1 | import { Category, CategoryKey } from '../types/challenge'; 2 | 3 | export const categoryMap: Record = { 4 | ALL: '전체', 5 | FOOD: '식비', 6 | HOBBY_LEISURE: '취미/여가', 7 | FASHION_BEAUTY: '패션/뷰티', 8 | TRANSPORTATION_AUTOMOBILE: '교통/차량', 9 | }; 10 | 11 | export const ALL = '전체'; 12 | 13 | export const ACTIVE = 'ACTIVE'; 14 | -------------------------------------------------------------------------------- /src/shared/components/toast/ToastContainer.tsx: -------------------------------------------------------------------------------- 1 | import { useToast } from '@/shared/hooks/useToast'; 2 | 3 | import { Toast } from './Toast'; 4 | 5 | export const ToastsContainer = () => { 6 | const { toastMessage } = useToast(); 7 | 8 | return ( 9 |
10 | {toastMessage && } 11 |
12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import type { Preview } from '@storybook/react'; 2 | import '@/styles/globals.css'; 3 | 4 | const preview: Preview = { 5 | parameters: { 6 | actions: { argTypesRegex: '^on[A-Z].*' }, 7 | controls: { 8 | matchers: { 9 | color: /(background|color)$/i, 10 | date: /Date$/, 11 | }, 12 | }, 13 | }, 14 | }; 15 | 16 | export default preview; 17 | -------------------------------------------------------------------------------- /src/features/feed/FeedUserImg.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | 3 | type FeedUserImgProps = { imgUrl: string }; 4 | 5 | export const FeedUserImg = ({ imgUrl }: FeedUserImgProps) => { 6 | return ( 7 |
8 | 9 |
10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /src/shared/store/user.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand'; 2 | import { devtools } from 'zustand/middleware'; 3 | 4 | import { UserProfile } from '../types/user'; 5 | 6 | type UserState = { 7 | user?: UserProfile; 8 | setUser: (user: UserProfile) => void; 9 | }; 10 | 11 | export const useUserStore = create()( 12 | devtools((set) => ({ 13 | setUser: (user) => set({ user }), 14 | })), 15 | ); 16 | -------------------------------------------------------------------------------- /public/svgs/icon-arrow-up-fill.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/service/spending.ts: -------------------------------------------------------------------------------- 1 | import { Spending } from '@/shared/types/spending'; 2 | 3 | import { httpClient } from '.'; 4 | 5 | export const addSpending = async ({ 6 | price, 7 | title, 8 | content, 9 | imageUrl, 10 | challengeId, 11 | evaluation, 12 | }: Spending) => { 13 | return httpClient.post(`/record/${challengeId}`, { 14 | price, 15 | title, 16 | content, 17 | evaluation, 18 | imgUrl: imageUrl, 19 | }); 20 | }; 21 | -------------------------------------------------------------------------------- /public/svgs/icon-loading.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/features/challenge/queryKey.ts: -------------------------------------------------------------------------------- 1 | import { ChallengeFilter } from '@/shared/types/challenge'; 2 | 3 | export const challengeKeys = { 4 | all: ['challenge'] as const, 5 | lists: () => [...challengeKeys.all, 'list'] as const, 6 | list: (filters: ChallengeFilter) => 7 | [...challengeKeys.lists(), filters] as const, 8 | details: () => [...challengeKeys.all, 'detail'] as const, 9 | detail: (id: number) => [...challengeKeys.details(), id] as const, 10 | }; 11 | -------------------------------------------------------------------------------- /src/shared/store/room.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand'; 2 | 3 | import { CHALLENGE_ID_MY_ROOM } from '../constants'; 4 | 5 | type State = { 6 | challengeId: number; 7 | }; 8 | 9 | type Action = { 10 | setChallengeId: (challengeId: State['challengeId']) => void; 11 | }; 12 | 13 | export const useRoom = create((set) => ({ 14 | challengeId: CHALLENGE_ID_MY_ROOM, 15 | setChallengeId: (challengeId) => set(() => ({ challengeId })), 16 | })); 17 | -------------------------------------------------------------------------------- /src/shared/utils/currency.ts: -------------------------------------------------------------------------------- 1 | type ConvertNumberToCurrencyProps = { 2 | value: number; 3 | unitOfCurrency: string; 4 | }; 5 | /** 6 | * 7 | * @example 8 | * convertNumberToCurrency({value: 10000, unitOfCurrency: '원'}) 9 | * return '10,000 원' 10 | */ 11 | export const convertNumberToCurrency = ({ 12 | value, 13 | unitOfCurrency, 14 | }: ConvertNumberToCurrencyProps): string => { 15 | return `${new Intl.NumberFormat('en-US').format(value)}${unitOfCurrency}`; 16 | }; 17 | -------------------------------------------------------------------------------- /src/pages/not-found/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import ErrorPageBuilder from '@/shared/components/error-page'; 4 | 5 | const NotFound = () => { 6 | return ( 7 | 16 | ); 17 | }; 18 | 19 | export default NotFound; 20 | -------------------------------------------------------------------------------- /src/shared/hooks/useScrollToBottom.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | 3 | const useScrollToBottom = ({ earlyReturn }: { earlyReturn?: boolean }) => { 4 | const bottomRef = useRef(null); 5 | 6 | useEffect(() => { 7 | if (!bottomRef.current || earlyReturn) { 8 | return; 9 | } 10 | 11 | bottomRef.current.scrollIntoView(); 12 | }, [[bottomRef.current]]); 13 | 14 | return { bottomRef }; 15 | }; 16 | 17 | export { useScrollToBottom }; 18 | -------------------------------------------------------------------------------- /src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .scrollbar-hide::-webkit-scrollbar { 6 | display: none; 7 | } 8 | 9 | input[type='file'] { 10 | display: none; 11 | } 12 | 13 | /* Chrome, Safari, Edge, Opera */ 14 | input::-webkit-outer-spin-button, 15 | input::-webkit-inner-spin-button { 16 | -webkit-appearance: none; 17 | margin: 0; 18 | } 19 | 20 | /* Firefox */ 21 | input[type='number'] { 22 | -moz-appearance: textfield; 23 | } 24 | -------------------------------------------------------------------------------- /src/mocks/handlers/handlers.ts: -------------------------------------------------------------------------------- 1 | import { challengeHandlers } from '@/mocks/handlers/challenge'; 2 | import { commentHandlers } from '@/mocks/handlers/comment'; 3 | import { emojiHandlers } from '@/mocks/handlers/emoji'; 4 | import { feedHandlers } from '@/mocks/handlers/feed'; 5 | import { userHandlers } from '@/mocks/handlers/user'; 6 | 7 | export const handlers = [ 8 | ...userHandlers, 9 | ...feedHandlers, 10 | ...emojiHandlers, 11 | ...commentHandlers, 12 | ...challengeHandlers, 13 | ]; 14 | -------------------------------------------------------------------------------- /src/pages/internal-server-error/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import ErrorPageBuilder from '@/shared/components/error-page'; 4 | 5 | const InternalServerError = () => { 6 | return ( 7 | 16 | ); 17 | }; 18 | 19 | export default InternalServerError; 20 | -------------------------------------------------------------------------------- /src/shared/store/toast.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand'; 2 | 3 | type State = { 4 | toastMessage: string | null; 5 | }; 6 | 7 | type Action = { 8 | setToastMessage: (toastMessage: State['toastMessage']) => void; 9 | }; 10 | 11 | const initialState = { 12 | toastMessage: null, 13 | }; 14 | 15 | export const useToastStore = create((set) => ({ 16 | ...initialState, 17 | setToastMessage: (message) => 18 | set(() => ({ 19 | toastMessage: message, 20 | })), 21 | })); 22 | -------------------------------------------------------------------------------- /src/shared/components/challenge/ChallengeNotFound.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | 3 | import { Button } from '../button'; 4 | 5 | export default function ChallengeNotFound() { 6 | return ( 7 |
8 |

9 | 아직 아무기록이 없어요 10 |

11 | 12 | 13 | 14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/shared/components/toast/Toast.tsx: -------------------------------------------------------------------------------- 1 | type ToastProps = { 2 | message: string; 3 | }; 4 | 5 | export const TOAST_DURATION = 3000; 6 | 7 | export const Toast = ({ message }: ToastProps) => { 8 | return ( 9 |
10 | {message} 11 |
12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /src/service/auth-refresh.ts: -------------------------------------------------------------------------------- 1 | import { AxiosInstance } from 'axios'; 2 | 3 | import { AuthRefreshResponse } from '@/lib/interfaces'; 4 | 5 | export const authRefresh = async ( 6 | axiosInstance: AxiosInstance, 7 | ): Promise => { 8 | const response = await axiosInstance.post( 9 | 'https://api.jalingobi.com/auth/refresh', 10 | undefined, 11 | { 12 | headers: { 13 | 'Content-Type': 'application/json', 14 | }, 15 | }, 16 | ); 17 | return response.data; 18 | }; 19 | -------------------------------------------------------------------------------- /src/service/image.ts: -------------------------------------------------------------------------------- 1 | import { PresignedUrlResponse } from '@/shared/types/image'; 2 | import { getExtension } from '@/shared/utils/file'; 3 | 4 | import { httpClient } from './index'; 5 | 6 | export const createPresignedUrl = async ( 7 | file: File, 8 | type: string, 9 | ): Promise => { 10 | const response = await httpClient.post('/image/presigned', { 11 | imageFileExtension: getExtension(file?.type).toUpperCase(), 12 | type, 13 | }); 14 | 15 | return response.data.result; 16 | }; 17 | -------------------------------------------------------------------------------- /src/lib/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | import { operations } from './api.interface'; 2 | 3 | export type UpdateChallenge = operations['updateChallenge']; 4 | export type AuthKakao = operations['authKakao']; 5 | export type AuthRefresh = operations['refreshToken']; 6 | 7 | export type AuthKakaoBody = 8 | AuthKakao['requestBody']['content']['application/json']; 9 | export type AuthKakaoResponse = AuthKakao['responses']['200']['content']['*/*']; 10 | export type AuthRefreshResponse = 11 | AuthRefresh['responses']['200']['content']['*/*']; 12 | -------------------------------------------------------------------------------- /src/shared/components/date-chip/index.tsx: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs'; 2 | 3 | type DateChipProps = { 4 | date: string | null; 5 | }; 6 | 7 | export const DateChip = ({ date }: DateChipProps) => { 8 | const convertedDate = dayjs(date).format('YYYY년 M월 DD일'); 9 | 10 | return ( 11 |
  • 12 |

    13 | {convertedDate} 14 |

    15 |
  • 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /src/shared/components/layout/BottomNavLayout.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | import { BottomNavigation } from '../navigation'; 4 | 5 | import styles from './BottomNavLayout.module.css'; 6 | 7 | type BottomNavLayoutProps = { 8 | children: ReactNode; 9 | }; 10 | 11 | export default function BottomNavLayout({ children }: BottomNavLayoutProps) { 12 | return ( 13 |
    14 |
    {children}
    15 | 16 |
    17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/shared/hooks/useClickOutside.ts: -------------------------------------------------------------------------------- 1 | import React, { RefObject } from 'react'; 2 | 3 | export const useClickOutside = ( 4 | ref: RefObject, 5 | callback: () => void, 6 | ) => { 7 | const handleClick = (e: MouseEvent) => { 8 | if (ref.current && !ref.current.contains(e.target as Node)) { 9 | callback(); 10 | } 11 | }; 12 | React.useEffect(() => { 13 | document.addEventListener('click', handleClick); 14 | return () => { 15 | document.removeEventListener('click', handleClick); 16 | }; 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /.commitlintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | rules: { 4 | 'type-enum': [ 5 | 2, 6 | 'always', 7 | [ 8 | 'build', 9 | 'chore', 10 | 'ci', 11 | 'docs', 12 | 'feat', 13 | 'fix', 14 | 'perf', 15 | 'refactor', 16 | 'revert', 17 | 'lint', 18 | 'style', 19 | 'test', 20 | 'add', 21 | 'update', 22 | 'design', 23 | 'remove', 24 | 'rename', 25 | ], 26 | ], 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /src/features/feed/FeedDate.tsx: -------------------------------------------------------------------------------- 1 | import { Spacing } from '@/shared/components'; 2 | import { DateChip } from '@/shared/components/date-chip'; 3 | import { isFeedDateDifferent } from '@/shared/utils/date/date'; 4 | 5 | type FeedDateProps = { 6 | currentFeedDate: string; 7 | nextFeedDate: string; 8 | }; 9 | 10 | export const FeedDate = ({ currentFeedDate, nextFeedDate }: FeedDateProps) => { 11 | return isFeedDateDifferent({ 12 | currentFeedDate, 13 | nextFeedDate, 14 | }) ? ( 15 | 16 | ) : ( 17 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /src/features/feed/FeedUserInfo.tsx: -------------------------------------------------------------------------------- 1 | type FeedUserInfoProps = { 2 | nickname: string; 3 | convertedCurrentCharge: string; 4 | }; 5 | 6 | export const FeedUserInfo = ({ 7 | nickname, 8 | convertedCurrentCharge, 9 | }: FeedUserInfoProps) => { 10 | return ( 11 |
    12 |

    13 | {nickname} 14 |

    15 |

    16 | {convertedCurrentCharge} 17 |

    18 |
    19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /public/svgs/icon-user.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/svgs/icon-chevron-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/shared/hof/withAuth.ts: -------------------------------------------------------------------------------- 1 | import { GetServerSideProps, GetServerSidePropsContext } from 'next'; 2 | 3 | import { httpClient } from '@/service'; 4 | 5 | export const withAuth = (handler: GetServerSideProps): GetServerSideProps => { 6 | return async (context: GetServerSidePropsContext) => { 7 | try { 8 | await httpClient.post('/auth/refresh', { headers: context.req.headers }); 9 | } catch (error) { 10 | return { 11 | redirect: { 12 | destination: '/auth/login', 13 | permanent: false, 14 | }, 15 | }; 16 | } 17 | 18 | return handler(context); 19 | }; 20 | }; 21 | -------------------------------------------------------------------------------- /public/svgs/icon-add-circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/shared/types/comment.ts: -------------------------------------------------------------------------------- 1 | import { ApiResponse } from './api'; 2 | 3 | export type AddCommentRequest = { 4 | content: string; 5 | recordId: number; 6 | }; 7 | 8 | export type AddCommentResult = { 9 | result: { 10 | commentDate: string; 11 | commenterId: number; 12 | content: string; 13 | id: number; 14 | imgUrl: string; 15 | nickname: string; 16 | }; 17 | }; 18 | 19 | export type AddCommentResponse = AddCommentRequest & AddCommentResult; 20 | 21 | export type DeleteCommentRequest = { 22 | recordId: number; 23 | commentId: number; 24 | }; 25 | 26 | export type DeleteCommentResult = ApiResponse<{ commentId: number }>; 27 | -------------------------------------------------------------------------------- /public/svgs/icon-reaction.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/mocks/handlers/emoji.ts: -------------------------------------------------------------------------------- 1 | import { rest } from 'msw'; 2 | 3 | export const emojiHandlers = [ 4 | rest.put(`/record/:recordId/emoji`, (req, res, ctx) => { 5 | return res( 6 | ctx.delay(500), 7 | ctx.status(200), 8 | ctx.json({ 9 | isSuccess: true, 10 | code: 200, 11 | message: '요청에 성공하였습니다.', 12 | }), 13 | ); 14 | }), 15 | rest.delete(`/record/:recordId/emoji`, (req, res, ctx) => { 16 | return res( 17 | ctx.delay(500), 18 | ctx.status(200), 19 | ctx.json({ 20 | isSuccess: true, 21 | code: 200, 22 | message: '요청에 성공하였습니다.', 23 | }), 24 | ); 25 | }), 26 | ]; 27 | -------------------------------------------------------------------------------- /public/svgs/icon-note.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/shared/utils/time/time.spec.ts: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs'; 2 | 3 | import { calculateDaysLeft } from './time'; 4 | 5 | describe('calculateDaysLeft 함수', () => { 6 | it('오늘 날짜와 미래 날짜 사이의 날짜를 계산한다', () => { 7 | const futureDate = dayjs().add(7, 'day').format('YYYY-MM-DD'); // 미래의 날짜 (7일 후) 8 | expect(calculateDaysLeft(futureDate)).toBe(7); 9 | }); 10 | 11 | it('입력 날짜가 오늘이거나 과거일 경우 0을 반환해야 한다.', () => { 12 | const pastDate = dayjs().subtract(7, 'day').format('YYYY-MM-DD'); // 과거의 날짜 (7일 전) 13 | expect(calculateDaysLeft(pastDate)).toBe(0); 14 | 15 | const today = dayjs().format('YYYY-MM-DD'); // 오늘 날짜 16 | expect(calculateDaysLeft(today)).toBe(0); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "baseUrl": ".", 18 | "paths": { 19 | "@/*": ["./src/*", "./*"] 20 | } 21 | }, 22 | "include": ["global.d.ts", "next-env.d.ts", "**/*.ts", "**/*.tsx"], 23 | "exclude": ["node_modules"] 24 | } 25 | -------------------------------------------------------------------------------- /src/styles/tailwind/color.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | white: '#FFFFFF', 3 | 'gray-5': '#F8F8FA', 4 | 'gray-10': '#F2F3F5', 5 | 'gray-20': '#E8EBF0', 6 | 'gray-30': '#DCE0E9', 7 | 'gray-40': '#C2C7D1', 8 | 'gray-50': '#9EA3AD', 9 | 'gray-60': '#73777E', 10 | 'gray-70': '#4B4E53', 11 | black: '#1E1E1E', 12 | 13 | 'primary-light': '#FFF3EB', 14 | primary: '#FF802D', 15 | 'primary-dark': '#CD5303', 16 | 'secondary-light': '#FFFAE9', 17 | secondary: '#FFE381', 18 | 'secondary-dark': '#F5CB37', 19 | 'accent-light': '#E2F2FF', 20 | accent: '#68BBFF', 21 | 'accent-dark': '#2092F0', 22 | 23 | 'system-danger': '#FD5056', 24 | 'system-success': '#38C77A', 25 | 'system-basic': '#FCF6F1', 26 | }; 27 | -------------------------------------------------------------------------------- /.github/workflows/chromatic.yml: -------------------------------------------------------------------------------- 1 | # Workflow name 2 | name: 'Chromatic Deployment' 3 | 4 | # Event for the workflow 5 | on: 6 | push: 7 | branches: 8 | - main 9 | paths: 10 | - '**.stories.tsx' 11 | 12 | jobs: 13 | test: 14 | # Operating System 15 | runs-on: ubuntu-latest 16 | # Job steps 17 | steps: 18 | - uses: actions/checkout@v3 19 | with: 20 | fetch-depth: 0 21 | - run: yarn 22 | #👇 Adds Chromatic as a step in the workflow 23 | - uses: chromaui/action@v1 24 | # Options required for Chromatic's GitHub Action 25 | with: 26 | projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} 27 | token: ${{ secrets.GITHUB_TOKEN }} 28 | -------------------------------------------------------------------------------- /src/shared/components/font/ApplyingFont.tsx: -------------------------------------------------------------------------------- 1 | import localFont from 'next/font/local'; 2 | import { ReactNode } from 'react'; 3 | 4 | type Props = { 5 | children: ReactNode; 6 | }; 7 | 8 | const pretendard = localFont({ 9 | src: [ 10 | { 11 | path: '../../../../public/fonts/Pretendard-Regular.woff2', 12 | weight: '400', 13 | style: 'normal', 14 | }, 15 | { 16 | path: '../../../../public/fonts/Pretendard-Medium.woff2', 17 | weight: '500', 18 | style: 'normal', 19 | }, 20 | ], 21 | variable: '--font-pretendard', 22 | }); 23 | 24 | export default function ApplyingFont({ children }: Props) { 25 | return
    {children}
    ; 26 | } 27 | -------------------------------------------------------------------------------- /public/svgs/icon-x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/features/comment/queries.ts: -------------------------------------------------------------------------------- 1 | import { useMutation, useQueryClient } from '@tanstack/react-query'; 2 | 3 | import { addComment, deleteComment } from '@/service/comment'; 4 | 5 | export const useAddComment = () => { 6 | const queryClient = useQueryClient(); 7 | 8 | return useMutation({ 9 | mutationFn: addComment, 10 | onSuccess: () => { 11 | queryClient.invalidateQueries({ queryKey: ['addComment'] }); 12 | }, 13 | }); 14 | }; 15 | 16 | export const useDeleteComment = () => { 17 | const queryClient = useQueryClient(); 18 | 19 | return useMutation({ 20 | mutationFn: deleteComment, 21 | onSuccess: () => { 22 | queryClient.invalidateQueries({ queryKey: ['deleteComment'] }); 23 | }, 24 | }); 25 | }; 26 | -------------------------------------------------------------------------------- /src/shared/components/challenge/Rules.tsx: -------------------------------------------------------------------------------- 1 | import { CheckIcon } from 'lucide-react'; 2 | 3 | type Props = { 4 | rules?: string[]; 5 | }; 6 | 7 | export default function Rules({ rules }: Props) { 8 | return ( 9 |
    10 |
    11 |

    규칙

    12 |
      13 | {rules?.map((rule, index) => ( 14 |
    • 15 | 16 | {rule} 17 |
    • 18 | ))} 19 |
    20 |
    21 |
    22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/service/comment.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AddCommentRequest, 3 | AddCommentResponse, 4 | DeleteCommentRequest, 5 | DeleteCommentResult, 6 | } from '@/shared/types/comment'; 7 | 8 | import { httpClient } from '.'; 9 | 10 | export const addComment = async ({ 11 | content, 12 | recordId, 13 | }: AddCommentRequest): Promise => { 14 | const response = await httpClient.post(`/record/${recordId}/comment`, { 15 | content, 16 | }); 17 | 18 | return response.data; 19 | }; 20 | 21 | export const deleteComment = async ({ 22 | recordId, 23 | commentId, 24 | }: DeleteCommentRequest): Promise => { 25 | const response = await httpClient.delete( 26 | `/record/${recordId}/comment/${commentId}`, 27 | ); 28 | 29 | return response.data; 30 | }; 31 | -------------------------------------------------------------------------------- /public/svgs/icon-check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/shared/components/challenge-list/index.tsx: -------------------------------------------------------------------------------- 1 | import { isEmpty } from 'lodash-es'; 2 | 3 | import ChallengeCard from '@/shared/components/challenge-card'; 4 | import { UserChallenge } from '@/shared/types/user'; 5 | 6 | import ChallengeNotFound from '../challenge/ChallengeNotFound'; 7 | 8 | type Props = { 9 | filteredCategoryList?: UserChallenge[]; 10 | }; 11 | 12 | const ChallengeList = ({ filteredCategoryList }: Props) => { 13 | if (isEmpty(filteredCategoryList)) { 14 | return ; 15 | } 16 | 17 | return ( 18 |
      19 | {filteredCategoryList?.map((challenge) => ( 20 | 21 | ))} 22 |
    23 | ); 24 | }; 25 | 26 | export default ChallengeList; 27 | -------------------------------------------------------------------------------- /src/service/emoji.ts: -------------------------------------------------------------------------------- 1 | import { ApiResponse } from '@/shared/types/api'; 2 | import { EmojiRequest } from '@/shared/types/emoji'; 3 | 4 | import { httpClient } from '.'; 5 | 6 | export const updateEmoji = async ({ 7 | recordId, 8 | type, 9 | }: // eslint-disable-next-line @typescript-eslint/ban-types 10 | EmojiRequest): Promise> => { 11 | const response = await httpClient.put(`/record/${recordId}/emoji`, { 12 | type, 13 | }); 14 | 15 | return response.data; 16 | }; 17 | 18 | export const deleteEmoji = async ({ 19 | recordId, 20 | type, 21 | }: // eslint-disable-next-line @typescript-eslint/ban-types 22 | EmojiRequest): Promise> => { 23 | const response = await httpClient.delete(`/record/${recordId}/emoji`, { 24 | data: { type }, 25 | }); 26 | 27 | return response.data; 28 | }; 29 | -------------------------------------------------------------------------------- /src/features/emoji/queries.ts: -------------------------------------------------------------------------------- 1 | import { useMutation, useQueryClient } from '@tanstack/react-query'; 2 | import { v4 as uuid4 } from 'uuid'; 3 | 4 | import { deleteEmoji, updateEmoji } from '@/service/emoji'; 5 | 6 | export const useUpdateEmoji = () => { 7 | const id = uuid4(); 8 | const queryClient = useQueryClient(); 9 | 10 | return useMutation({ 11 | mutationFn: updateEmoji, 12 | onSuccess: () => { 13 | queryClient.invalidateQueries({ queryKey: ['useUpdateEmoji', id] }); 14 | }, 15 | }); 16 | }; 17 | 18 | export const useDeleteEmoji = () => { 19 | const id = uuid4(); 20 | const queryClient = useQueryClient(); 21 | 22 | return useMutation({ 23 | mutationFn: deleteEmoji, 24 | onSuccess: () => { 25 | queryClient.invalidateQueries({ queryKey: ['juseDeleteEmoji', id] }); 26 | }, 27 | }); 28 | }; 29 | -------------------------------------------------------------------------------- /src/shared/components/button/Button.stories.tsx: -------------------------------------------------------------------------------- 1 | // Button.stories.ts|tsx 2 | 3 | import type { Meta, StoryObj } from '@storybook/react'; 4 | 5 | import { IconSearch } from '@/public/svgs'; 6 | import { Button } from '@/shared/components/button/Button'; 7 | 8 | const meta: Meta = { 9 | title: 'ButtonTest', 10 | component: Button, 11 | }; 12 | 13 | export default meta; 14 | type Story = StoryObj; 15 | 16 | export const Contained: Story = { 17 | args: { 18 | variant: 'primary', 19 | size: 'md', 20 | children: 'Primary', 21 | }, 22 | }; 23 | 24 | export const WithIcon: Story = { 25 | args: { 26 | variant: 'primary', 27 | size: 'md', 28 | children: ( 29 | <> 30 | 31 | Button Label 32 | 33 | ), 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /src/shared/components/form/schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | import { contentMaxLength, maxPrice } from '@/shared/constants/spending'; 4 | 5 | export const spendSchema = z.object({ 6 | price: z 7 | .string() 8 | .transform((year) => Number(year)) 9 | .pipe( 10 | z 11 | .number() 12 | .positive({ 13 | message: '금액은 0원 이상이어야 합니다.', 14 | }) 15 | .max(maxPrice, { 16 | message: `${maxPrice}원 이하로 입력해주세요`, 17 | }), 18 | ) 19 | .transform((year) => year.toString()), 20 | title: z 21 | .string() 22 | .max(16, { 23 | message: '지출명은 16자 이내로 입력해주세요', 24 | }) 25 | .min(1, { 26 | message: '지출명을 입력해주세요', 27 | }), 28 | content: z.string().max(contentMaxLength, { 29 | message: `${contentMaxLength}자 이하로 작성해주세요`, 30 | }), 31 | }); 32 | -------------------------------------------------------------------------------- /src/shared/components/label/Label.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import * as LabelPrimitive from '@radix-ui/react-label'; 4 | import { cva, type VariantProps } from 'class-variance-authority'; 5 | 6 | import { cn } from '@/lib/utils'; 7 | 8 | const labelVariants = cva( 9 | 'font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70', 10 | ); 11 | 12 | const Label = React.forwardRef< 13 | React.ElementRef, 14 | React.ComponentPropsWithoutRef & 15 | VariantProps 16 | >(({ className, ...props }, ref) => ( 17 | 22 | )); 23 | Label.displayName = LabelPrimitive.Root.displayName; 24 | 25 | export { Label }; 26 | -------------------------------------------------------------------------------- /src/shared/components/input/Input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ); 21 | }, 22 | ); 23 | Input.displayName = 'Input'; 24 | 25 | export { Input }; 26 | -------------------------------------------------------------------------------- /src/shared/components/textarea/Textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes {} 7 | 8 | const Textarea = React.forwardRef( 9 | ({ className, ...props }, ref) => { 10 | return ( 11 |