;
14 |
15 | export const Primary: Story = {
16 | render: () => ,
17 | };
18 |
19 | export const Selected: Story = {
20 | render: () => ,
21 | };
22 |
23 | export const WithCancelIcon: Story = {
24 | render: () => ,
25 | };
26 | export const WithPlusIcon: Story = {
27 | render: () => ,
28 | };
29 |
30 | export const SelectedWithIcon: Story = {
31 | render: () => ,
32 | };
33 |
--------------------------------------------------------------------------------
/src/components/Chip/index.ts:
--------------------------------------------------------------------------------
1 | export { Chip } from './Chip';
2 |
--------------------------------------------------------------------------------
/src/components/ConfirmPopup/ConfirmCreateIdCard/ConfirmCreateIdCard.client.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { Button } from '~/components/Button';
3 | import { TextButton } from '~/components/Button';
4 | import { ConfirmType } from '~/components/ConfirmPopup/useConfirmPopup';
5 | import Popup from '~/components/Popup/Popup';
6 |
7 | // const TITLE = '주민증이 없으면 댓글을 남길 수 없어요';
8 | const DESC = '주민증이 없으면 댓글을 남길 수 없어요';
9 |
10 | type ConfirmCreateIdCardProps = {
11 | confirm: (type: ConfirmType) => void;
12 | };
13 |
14 | export const ConfirmCreateIdCard = ({ confirm }: ConfirmCreateIdCardProps) => {
15 | const buttons = (
16 |
17 |
20 | confirm('CANCEL')}
22 | type="button"
23 | className="rounded-xl pt-13pxr text-b3 text-grey-900"
24 | >
25 | 다음에 만들기
26 |
27 |
28 | );
29 |
30 | return ;
31 | };
32 |
--------------------------------------------------------------------------------
/src/components/ConfirmPopup/ConfirmCreateIdCard/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ConfirmCreateIdCard.client';
2 |
--------------------------------------------------------------------------------
/src/components/ConfirmPopup/ConfirmDeleteKeyword/ConfirmDeleteKeyword.client.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { Button } from '~/components/Button';
3 | import { TextButton } from '~/components/Button';
4 | import { ConfirmType } from '~/components/ConfirmPopup/useConfirmPopup';
5 | import Popup from '~/components/Popup/Popup';
6 |
7 | const TITLE = '키워드를 삭제하시겠어요?';
8 | const DESC = '키워드 세부 내용까지 없어져요';
9 |
10 | type ConfirmDeleteKeywordProps = {
11 | confirm: (type: ConfirmType) => void;
12 | };
13 |
14 | export const ConfirmDeleteKeyword = ({ confirm }: ConfirmDeleteKeywordProps) => {
15 | const buttons = (
16 |
17 |
20 | confirm('OK')}
22 | type="button"
23 | className="rounded-xl pt-13pxr text-b3 text-grey-900"
24 | >
25 | 삭제하기
26 |
27 |
28 | );
29 |
30 | return ;
31 | };
32 |
--------------------------------------------------------------------------------
/src/components/ConfirmPopup/ConfirmDeleteKeyword/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ConfirmDeleteKeyword.client';
2 |
--------------------------------------------------------------------------------
/src/components/ConfirmPopup/ConfirmUnSave/ConfirmUnSave.client.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { Button } from '~/components/Button';
3 | import { TextButton } from '~/components/Button';
4 | import { ConfirmType } from '~/components/ConfirmPopup/useConfirmPopup';
5 | import Popup from '~/components/Popup/Popup';
6 |
7 | const TITLE = '저장되지 않은 변경사항이 있어요';
8 | const DESC = '변경 사항을 취소할까요?';
9 |
10 | type ConfirmUnSaveProps = {
11 | confirm: (type: ConfirmType) => void;
12 | };
13 |
14 | export const ConfirmUnSave = ({ confirm }: ConfirmUnSaveProps) => {
15 | const buttons = (
16 |
17 |
20 | confirm('OK')}
22 | type="button"
23 | className="rounded-xl pt-13pxr text-b3 text-grey-900"
24 | >
25 | 네, 저장하지 않을래요
26 |
27 |
28 | );
29 |
30 | return ;
31 | };
32 |
--------------------------------------------------------------------------------
/src/components/ConfirmPopup/ConfirmUnSave/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ConfirmUnSave.client';
2 |
--------------------------------------------------------------------------------
/src/components/ConfirmPopup/CopyInvitation/CopyInvitation.client.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { Button } from '~/components/Button';
3 | import { TextButton } from '~/components/Button';
4 | import { ConfirmType } from '~/components/ConfirmPopup/useConfirmPopup';
5 | import Popup from '~/components/Popup/Popup';
6 |
7 | const TITLE = '초대 링크 복사 완료';
8 | const DESC = '초대 링크를 공유해 보세요';
9 |
10 | type CopyInvitationProps = {
11 | confirm: (type: ConfirmType) => void;
12 | };
13 |
14 | export const CopyInvitation = ({ confirm }: CopyInvitationProps) => {
15 | const buttons = (
16 |
17 |
20 | confirm('CANCEL')}
22 | type="button"
23 | className="rounded-xl pt-13pxr text-b3 text-grey-900"
24 | >
25 | 닫기
26 |
27 |
28 | );
29 |
30 | return ;
31 | };
32 |
--------------------------------------------------------------------------------
/src/components/ConfirmPopup/CopyInvitation/index.ts:
--------------------------------------------------------------------------------
1 | export * from './CopyInvitation.client';
2 |
--------------------------------------------------------------------------------
/src/components/ConfirmPopup/SimpleConfirmPopup/SimpleConfirmPopup.client.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { Button } from '~/components/Button';
3 | import { TextButton } from '~/components/Button';
4 | import { ConfirmType } from '~/components/ConfirmPopup/useConfirmPopup';
5 | import Popup from '~/components/Popup/Popup';
6 |
7 | type SimpleConfirmPopupProps = {
8 | confirm: (type: ConfirmType) => void;
9 | title: string;
10 | description: string;
11 | confirmText?: string;
12 | cancelText?: string;
13 | };
14 |
15 | export const SimpleConfirmPopup = ({
16 | confirm,
17 | title,
18 | description,
19 | confirmText,
20 | cancelText,
21 | }: SimpleConfirmPopupProps) => {
22 | const buttons = (
23 |
24 |
27 | confirm('CANCEL')}
29 | type="button"
30 | className="rounded-xl pt-13pxr text-b3 text-grey-900"
31 | >
32 | {cancelText}
33 |
34 |
35 | );
36 |
37 | return ;
38 | };
39 |
--------------------------------------------------------------------------------
/src/components/ConfirmPopup/SimpleConfirmPopup/index.ts:
--------------------------------------------------------------------------------
1 | export * from './SimpleConfirmPopup.client';
2 |
--------------------------------------------------------------------------------
/src/components/ConfirmPopup/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ConfirmDeleteKeyword';
2 | export * from './ConfirmUnSave';
3 | export * from './CopyInvitation';
4 | export * from './SimpleConfirmPopup';
5 | export * from './useConfirmPopup';
6 |
--------------------------------------------------------------------------------
/src/components/ConfirmPopup/useConfirmPopup.ts:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 |
3 | /**
4 | * 단순 실행만 할 경우 "OPEN"
5 | */
6 | export type ConfirmType = 'OK' | 'CANCEL' | 'OPEN';
7 |
8 | export const useConfirmPopup = (initialState = false) => {
9 | const [isOpen, setIsOpen] = useState(initialState);
10 |
11 | const [resolveFunc, setResolveFunc] = useState<((result: boolean) => void) | null>(null);
12 |
13 | const openPopup = () => {
14 | setIsOpen(true);
15 |
16 | return new Promise(resolve => {
17 | setResolveFunc(() => resolve);
18 | });
19 | };
20 |
21 | const closePopup = () => {
22 | setIsOpen(false);
23 | };
24 |
25 | const confirm = (type: ConfirmType = 'OPEN') => {
26 | const isOk = type === 'OK';
27 | if (resolveFunc) {
28 | resolveFunc(isOk);
29 | setResolveFunc(null);
30 | }
31 | };
32 |
33 | return {
34 | isOpen,
35 | openPopup,
36 | closePopup,
37 | confirm,
38 | };
39 | };
40 |
--------------------------------------------------------------------------------
/src/components/Divider/Divider.tsx:
--------------------------------------------------------------------------------
1 | import { tw } from '~/utils/tailwind.util';
2 |
3 | type DividerProps = {
4 | className?: string;
5 | };
6 |
7 | export const Divider = ({ className = 'bg-grey-100' }: DividerProps) => {
8 | return ;
9 | };
10 |
--------------------------------------------------------------------------------
/src/components/Divider/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Divider';
2 |
--------------------------------------------------------------------------------
/src/components/ErrorBoundary/RetryErrorBoundary.client.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { ErrorBoundary } from 'react-error-boundary';
4 |
5 | const RetryErrorBoundary = ({ children }: { children: React.ReactNode }) => {
6 | return (
7 | (
9 |
10 |
문제가 발생했습니다
11 |
페이지를 불러오는데 실패했습니다.
12 |
18 |
19 | )}
20 | >
21 | {children}
22 |
23 | );
24 | };
25 |
26 | export default RetryErrorBoundary;
27 |
--------------------------------------------------------------------------------
/src/components/ErrorBoundary/index.ts:
--------------------------------------------------------------------------------
1 | export * from './RetryErrorBoundary.client';
2 |
--------------------------------------------------------------------------------
/src/components/HydrationProvider/HydrationProvider.tsx:
--------------------------------------------------------------------------------
1 | import { dehydrate, Hydrate, QueryClient } from '@tanstack/react-query';
2 | import { type QueryFunction, QueryKey } from '@tanstack/react-query';
3 | import { cache, PropsWithChildren } from 'react';
4 |
5 | type HydrationProviderProps = {
6 | queryKey: QueryKey;
7 | queryFn: QueryFunction;
8 | };
9 |
10 | export const HydrationProvider = async ({
11 | children,
12 | queryKey,
13 | queryFn,
14 | }: PropsWithChildren) => {
15 | const getQueryClient = cache(() => new QueryClient());
16 |
17 | const queryClient = getQueryClient();
18 | await queryClient.prefetchQuery(queryKey, queryFn);
19 | const dehydratedState = dehydrate(queryClient);
20 |
21 | return {children};
22 | };
23 |
--------------------------------------------------------------------------------
/src/components/HydrationProvider/index.ts:
--------------------------------------------------------------------------------
1 | export * from './HydrationProvider';
2 |
--------------------------------------------------------------------------------
/src/components/Icon/ArrowVerticalIcon.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentProps } from 'react';
2 |
3 | import { Svg } from '~/components/Svg';
4 | import { tw } from '~/utils/tailwind.util';
5 |
6 | export const ArrowVerticalIcon = ({
7 | className,
8 | size,
9 | width,
10 | height,
11 | ...rest
12 | }: ComponentProps) => {
13 | return (
14 |
38 | );
39 | };
40 |
--------------------------------------------------------------------------------
/src/components/Icon/CancelIcon.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentProps } from 'react';
2 |
3 | import { Svg } from '~/components/Svg';
4 | import { tw } from '~/utils/tailwind.util';
5 |
6 | export const CancelIcon = ({
7 | className,
8 | size,
9 | width,
10 | height,
11 | ...rest
12 | }: ComponentProps) => {
13 | return (
14 |
24 | );
25 | };
26 |
27 | export const CancelBoldIcon = ({
28 | className,
29 | size,
30 | width,
31 | height,
32 | ...rest
33 | }: ComponentProps) => {
34 | return (
35 |
45 | );
46 | };
47 |
--------------------------------------------------------------------------------
/src/components/Icon/CheckCircleFillIcon.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentProps } from 'react';
2 |
3 | import { Svg } from '~/components/Svg';
4 | import { tw } from '~/utils/tailwind.util';
5 |
6 | export const CheckCircleFillIcon = ({
7 | className,
8 | size,
9 | width,
10 | height,
11 | ...rest
12 | }: ComponentProps) => {
13 | return (
14 |
23 | );
24 | };
25 |
--------------------------------------------------------------------------------
/src/components/Icon/ChevronLeftIcon.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentProps } from 'react';
2 |
3 | import { Svg } from '~/components/Svg';
4 |
5 | export const ChevronLeftIcon = ({
6 | className,
7 | size,
8 | width,
9 | height,
10 | ...rest
11 | }: ComponentProps) => {
12 | return (
13 |
19 | );
20 | };
21 |
--------------------------------------------------------------------------------
/src/components/Icon/ChevronRightIcon.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentProps } from 'react';
2 |
3 | import { Svg } from '~/components/Svg';
4 |
5 | export const ChevronRightIcon = ({
6 | className,
7 | size,
8 | width,
9 | height,
10 | ...rest
11 | }: ComponentProps) => {
12 | return (
13 |
19 | );
20 | };
21 |
--------------------------------------------------------------------------------
/src/components/Icon/DashIcon.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentProps } from 'react';
2 |
3 | import { Svg } from '~/components/Svg';
4 |
5 | export const DashIcon = ({
6 | className,
7 | size,
8 | width,
9 | height,
10 | ...rest
11 | }: ComponentProps) => {
12 | return (
13 |
16 | );
17 | };
18 |
--------------------------------------------------------------------------------
/src/components/Icon/EyeIcon.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentProps } from 'react';
2 |
3 | import { Svg } from '~/components/Svg';
4 |
5 | export const EyeIcon = ({ ...rest }: ComponentProps) => {
6 | return (
7 |
23 | );
24 | };
25 |
--------------------------------------------------------------------------------
/src/components/Icon/HeartFillIcon.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentProps } from 'react';
2 |
3 | import { Svg } from '~/components/Svg';
4 |
5 | export const HeartFillIcon = ({
6 | className,
7 | size,
8 | width,
9 | height,
10 | ...rest
11 | }: ComponentProps) => {
12 | return (
13 |
19 | );
20 | };
21 |
--------------------------------------------------------------------------------
/src/components/Icon/HeartIcon.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentProps } from 'react';
2 |
3 | import { Svg } from '~/components/Svg';
4 |
5 | export const HeartIcon = ({
6 | className,
7 | size,
8 | width,
9 | height,
10 | ...rest
11 | }: ComponentProps) => {
12 | return (
13 |
16 | );
17 | };
18 |
--------------------------------------------------------------------------------
/src/components/Icon/HomeIcon.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentProps } from 'react';
2 |
3 | import { Svg } from '~/components/Svg';
4 |
5 | export const HomeIcon = ({
6 | className,
7 | size,
8 | width,
9 | height,
10 | ...rest
11 | }: ComponentProps) => {
12 | return (
13 |
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/src/components/Icon/KakaoIcon.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentProps } from 'react';
2 |
3 | import { Svg } from '~/components/Svg';
4 |
5 | export const KakaoIcon = ({
6 | className,
7 | size,
8 | width,
9 | height,
10 | ...rest
11 | }: ComponentProps) => {
12 | return (
13 |
19 | );
20 | };
21 |
--------------------------------------------------------------------------------
/src/components/Icon/NudgeCloseIcon.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentProps } from 'react';
2 |
3 | import { Svg } from '~/components/Svg';
4 |
5 | export const NudgeCloseIcon = ({ ...rest }: ComponentProps) => {
6 | return (
7 |
20 | );
21 | };
22 |
--------------------------------------------------------------------------------
/src/components/Icon/NudgeMessageIcon.tsx:
--------------------------------------------------------------------------------
1 | import { Svg } from '~/components/Svg';
2 |
3 | export const NudgeMessageIcon = () => {
4 | return (
5 |
22 | );
23 | };
24 |
--------------------------------------------------------------------------------
/src/components/Icon/PersonIcon.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentProps } from 'react';
2 |
3 | import { Svg } from '~/components/Svg';
4 |
5 | export const PersonIcon = ({
6 | className,
7 | size,
8 | width,
9 | height,
10 | ...rest
11 | }: ComponentProps) => {
12 | return (
13 |
23 | );
24 | };
25 |
--------------------------------------------------------------------------------
/src/components/Icon/PlusIcon.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentProps } from 'react';
2 |
3 | import { Svg } from '~/components/Svg';
4 |
5 | export const PlusIcon = ({
6 | className,
7 | size,
8 | width,
9 | height,
10 | ...rest
11 | }: ComponentProps) => {
12 | return (
13 |
16 | );
17 | };
18 |
--------------------------------------------------------------------------------
/src/components/Icon/QuestionCircleIcon.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentProps } from 'react';
2 |
3 | import { Svg } from '~/components/Svg';
4 |
5 | export const QuestionCircleIcon = ({
6 | className,
7 | size,
8 | width,
9 | height,
10 | ...rest
11 | }: ComponentProps) => {
12 | return (
13 |
17 | );
18 | };
19 |
--------------------------------------------------------------------------------
/src/components/Icon/SendIcon.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentProps } from 'react';
2 |
3 | import { Svg } from '~/components/Svg';
4 |
5 | export const SendIcon = ({
6 | className,
7 | size,
8 | width,
9 | height,
10 | ...rest
11 | }: ComponentProps) => {
12 | return (
13 |
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/src/components/Icon/ThreeDotsVerticalIcon.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentProps } from 'react';
2 |
3 | import { Svg } from '~/components/Svg';
4 | import { tw } from '~/utils/tailwind.util';
5 |
6 | export const ThreeDotsVerticalIcon = ({
7 | className,
8 | size,
9 | width,
10 | height,
11 | ...rest
12 | }: ComponentProps) => {
13 | return (
14 |
23 | );
24 | };
25 |
--------------------------------------------------------------------------------
/src/components/Icon/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './ArrowLeftIcon';
2 | export * from './ArrowVerticalIcon';
3 | export * from './CameraIcon';
4 | export * from './CancelCircleIcon';
5 | export * from './CancelIcon';
6 | export * from './CelebrationIcon';
7 | export * from './ChatBubbleIcon';
8 | export * from './CheckCircleFillIcon';
9 | export * from './ChevronLeftIcon';
10 | export * from './ChevronRightIcon';
11 | export * from './DashIcon';
12 | export * from './EyeIcon';
13 | export * from './GearIcon';
14 | export * from './HeartExchangeIcon';
15 | export * from './HeartFillIcon';
16 | export * from './HeartIcon';
17 | export * from './HomeIcon';
18 | export * from './NotificationIcon';
19 | export * from './NudgeCloseIcon';
20 | export * from './NudgeIcon';
21 | export * from './NudgeMessageIcon';
22 | export * from './PersonIcon';
23 | export * from './PlusIcon';
24 | export * from './QuestionCircleIcon';
25 | export * from './RiceIcon';
26 | export * from './SendIcon';
27 | export * from './ThreeDotsVerticalIcon';
28 |
--------------------------------------------------------------------------------
/src/components/KeywordInput/index.ts:
--------------------------------------------------------------------------------
1 | export * from './KeywordInput';
2 | export * from './keywordInput.type';
3 |
--------------------------------------------------------------------------------
/src/components/KeywordInput/keywordInput.type.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 확장성을 고려해서 객체 타입으로 만들었습니다.
3 | */
4 | export type OptionType = {
5 | title: string;
6 | imageUrl: string;
7 | content: string;
8 | };
9 |
--------------------------------------------------------------------------------
/src/components/Menu/Menu.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { Menu } from './index';
4 |
5 | const meta: Meta = {
6 | title: 'components/Menu',
7 | component: Menu,
8 | args: {},
9 | };
10 |
11 | export default meta;
12 |
13 | type Story = StoryObj;
14 |
15 | export const Primary: Story = {
16 | render: () => (
17 |
23 | ),
24 | };
25 |
--------------------------------------------------------------------------------
/src/components/Menu/MenuElement.tsx:
--------------------------------------------------------------------------------
1 | import { HTMLAttributes, PropsWithChildren } from 'react';
2 |
3 | import { ChevronRightIcon } from '~/components/Icon';
4 | import { tw } from '~/utils/tailwind.util';
5 |
6 | export const MenuElement = ({
7 | className,
8 | children,
9 | ...rest
10 | }: PropsWithChildren>) => {
11 | return (
12 |
20 | {children}
21 |
22 |
23 | );
24 | };
25 |
--------------------------------------------------------------------------------
/src/components/Menu/MenuHeader.tsx:
--------------------------------------------------------------------------------
1 | import { HTMLAttributes, PropsWithChildren } from 'react';
2 |
3 | import { tw } from '~/utils/tailwind.util';
4 |
5 | export const MenuHeader = ({
6 | className,
7 | children,
8 | ...rest
9 | }: PropsWithChildren>) => {
10 | return (
11 |
12 | {children}
13 |
14 | );
15 | };
16 |
--------------------------------------------------------------------------------
/src/components/Menu/MenuWrapper.tsx:
--------------------------------------------------------------------------------
1 | import { HTMLAttributes, PropsWithChildren } from 'react';
2 |
3 | export const MenuWrapper = ({
4 | className,
5 | children,
6 | ...rest
7 | }: PropsWithChildren>) => {
8 | return (
9 |
12 | );
13 | };
14 |
--------------------------------------------------------------------------------
/src/components/Menu/index.ts:
--------------------------------------------------------------------------------
1 | import { MenuElement } from '~/components/Menu/MenuElement';
2 | import { MenuHeader } from '~/components/Menu/MenuHeader';
3 | import { MenuWrapper } from '~/components/Menu/MenuWrapper';
4 |
5 | export const Menu = Object.assign(MenuWrapper, {
6 | Header: MenuHeader,
7 | Element: MenuElement,
8 | });
9 |
--------------------------------------------------------------------------------
/src/components/NudgeIconSelector/NudgeIconSelector.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | CelebrationIcon,
3 | EyeIcon,
4 | HeartExchangeIcon,
5 | NudgeIcon,
6 | RiceIcon,
7 | } from '~/components/Icon';
8 | import { NudgeIconSelectorType } from '~/types/nudge';
9 | import { ClassNameType } from '~/types/util';
10 |
11 | type NudgeIconSelectorProps = {
12 | nudgeType: NudgeIconSelectorType;
13 | className?: ClassNameType;
14 | };
15 |
16 | type IconWithProps = {
17 | className: ClassNameType;
18 | };
19 |
20 | const bellIconMap: Record> = {
21 | DEFAULT: ({ className, ...rest }) => ,
22 | MEET: ({ className, ...rest }) => ,
23 | FRIENDLY: ({ className, ...rest }) => ,
24 | SIMILARITY: ({ className, ...rest }) => ,
25 | TALKING: ({ className, ...rest }) => ,
26 | };
27 |
28 | export const NudgeIconSelector = ({ nudgeType, className, ...rest }: NudgeIconSelectorProps) => {
29 | const IconComponent = bellIconMap[nudgeType];
30 | return ;
31 | };
32 |
--------------------------------------------------------------------------------
/src/components/NudgeIconSelector/index.tsx:
--------------------------------------------------------------------------------
1 | export { NudgeIconSelector } from './NudgeIconSelector';
2 |
--------------------------------------------------------------------------------
/src/components/Popup/Popup.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { Button } from '~/components/Button';
4 |
5 | import Popup from './Popup';
6 |
7 | const meta: Meta = {
8 | title: 'components/Popup',
9 | component: Popup,
10 | };
11 |
12 | type Story = StoryObj;
13 |
14 | export const Default: Story = {
15 | args: {
16 | title: '타이틀',
17 | description: '세부 내용',
18 | buttons: (
19 | <>
20 |
23 |
26 | >
27 | ),
28 | },
29 | };
30 |
31 | export default meta;
32 |
33 | export const Positive: Story = {
34 | args: {
35 | title: '타이틀',
36 | description: '세부 내용',
37 | buttons: (
38 | <>
39 |
42 | >
43 | ),
44 | },
45 | };
46 |
--------------------------------------------------------------------------------
/src/components/Popup/Popup.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { ReactNode } from 'react';
4 |
5 | import { AnimatedPortal } from '~/components/Portal';
6 |
7 | export type PopupProps = {
8 | title?: string;
9 | description?: string;
10 | buttons?: ReactNode;
11 | };
12 |
13 | const Popup = ({ title, description, buttons }: PopupProps) => {
14 | return (
15 |
18 |
19 |
20 | {title &&
{title}
}
21 | {description &&
{description}
}
22 | {buttons &&
{buttons}
}
23 |
24 |
25 |
26 | );
27 | };
28 |
29 | export default Popup;
30 |
--------------------------------------------------------------------------------
/src/components/Popup/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './Popup';
2 |
--------------------------------------------------------------------------------
/src/components/Popup/usePopup.ts:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 |
3 | const usePopup = (initialState = false) => {
4 | const [isOpen, setIsOpen] = useState(initialState);
5 |
6 | const open = () => setIsOpen(true);
7 | const close = () => setIsOpen(false);
8 |
9 | return {
10 | isOpen,
11 | open,
12 | close,
13 | };
14 | };
15 |
16 | export default usePopup;
17 |
--------------------------------------------------------------------------------
/src/components/Portal/AnimatedPortal.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { AnimatePresence, motion, MotionProps } from 'framer-motion';
4 | import { PropsWithChildren } from 'react';
5 |
6 | import { Portal } from '~/components/Portal';
7 |
8 | type AnimatedPortalProps = {
9 | motionProps: MotionProps;
10 | } & PropsWithChildren;
11 |
12 | export const AnimatedPortal = ({ children, motionProps }: AnimatedPortalProps) => {
13 | return (
14 |
15 |
16 | {children}
17 |
18 |
19 | );
20 | };
21 |
--------------------------------------------------------------------------------
/src/components/Portal/Portal.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { PropsWithChildren, useEffect, useRef } from 'react';
4 | import { createPortal } from 'react-dom';
5 |
6 | import useIsMounted from '~/hooks/useIsMounted.hooks';
7 |
8 | export type PortalProps = {
9 | documentId?: string;
10 | };
11 |
12 | const findWrapperElement = (documentId: string): Element | null => {
13 | const wrapper = document.getElementById(documentId);
14 | if (wrapper) {
15 | return wrapper;
16 | } else {
17 | console.warn(`Element with ID '${documentId}'가 root layout에 없어요....추가해주세요.`);
18 | return null;
19 | }
20 | };
21 |
22 | export const Portal = ({ documentId, children }: PropsWithChildren) => {
23 | const ref = useRef(null);
24 | const isMounted = useIsMounted();
25 |
26 | useEffect(() => {
27 | if (documentId) {
28 | const wrapper = findWrapperElement(documentId);
29 | ref.current = wrapper;
30 | } else {
31 | ref.current = findWrapperElement('portal');
32 | }
33 | }, [isMounted, documentId]);
34 |
35 | if (!(isMounted && ref.current)) return null;
36 |
37 | return createPortal(children, ref.current);
38 | };
39 |
--------------------------------------------------------------------------------
/src/components/Portal/index.ts:
--------------------------------------------------------------------------------
1 | export * from './AnimatedPortal';
2 | export * from './Portal';
3 |
--------------------------------------------------------------------------------
/src/components/ProfileImageEdit/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ProfileImageEdit.client';
2 |
--------------------------------------------------------------------------------
/src/components/ProgressBar/ProgressBar.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 | import { useState } from 'react';
3 |
4 | import { ProgressBar } from './ProgressBar';
5 |
6 | const meta: Meta = {
7 | title: 'components/ProgressBar',
8 | component: ProgressBar,
9 | };
10 | export default meta;
11 |
12 | type Story = StoryObj;
13 |
14 | export const Defulat: Story = {
15 | render: () => {
16 | const stepsLength = 5;
17 | // eslint-disable-next-line react-hooks/rules-of-hooks
18 | const [currentStep, setCurrentStep] = useState(1);
19 |
20 | return (
21 |
22 |
23 |
24 |
{currentStep}
25 |
26 |
27 | );
28 | },
29 | };
30 |
--------------------------------------------------------------------------------
/src/components/ProgressBar/ProgressBar.tsx:
--------------------------------------------------------------------------------
1 | type ProgressBarProps = {
2 | currentStep: number;
3 | stepsLength: number;
4 | };
5 |
6 | export const ProgressBar = ({ currentStep, stepsLength }: ProgressBarProps) => {
7 | const percentage = (currentStep / stepsLength) * 100;
8 |
9 | return (
10 |
16 | );
17 | };
18 |
--------------------------------------------------------------------------------
/src/components/ProgressBar/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ProgressBar';
2 |
--------------------------------------------------------------------------------
/src/components/SpeechBubble/SpeechBubbleThumbnail.tsx:
--------------------------------------------------------------------------------
1 | import { NudgeIconSelector } from '~/components/NudgeIconSelector';
2 | import { nudgeMessages, NudgeModel } from '~/types/nudge';
3 | import { tw } from '~/utils/tailwind.util';
4 |
5 | type SpeechBubbleThumbnailProps = {
6 | nudgeType: NudgeModel;
7 | };
8 |
9 | export const SpeechBubbleThumbnail = ({ nudgeType }: SpeechBubbleThumbnailProps) => {
10 | const message = nudgeMessages.find(x => x.id === nudgeType)?.text;
11 |
12 | return (
13 |
14 |
15 |
16 | <>
17 | {message}
18 | 를 나에게 보냈어요
19 | >
20 |
21 |
24 |
25 | );
26 | };
27 |
--------------------------------------------------------------------------------
/src/components/SpeechBubble/index.ts:
--------------------------------------------------------------------------------
1 | import { SpeechBubbleDetail } from '~/components/SpeechBubble/SpeechBubbleDetail';
2 | import { SpeechBubbleThumbnail } from '~/components/SpeechBubble/SpeechBubbleThumbnail';
3 |
4 | export const SpeechBubble = Object.assign(
5 | {},
6 | {
7 | Detail: SpeechBubbleDetail,
8 | Thumbnail: SpeechBubbleThumbnail,
9 | },
10 | );
11 |
--------------------------------------------------------------------------------
/src/components/Svg/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './Svg';
2 |
--------------------------------------------------------------------------------
/src/components/Swiper/Swiper.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import 'swiper/css';
4 | import 'swiper/css/navigation';
5 | import 'swiper/css/pagination';
6 | import 'swiper/css/scrollbar';
7 |
8 | import { PropsWithChildren, useRef } from 'react';
9 | import { Navigation, Pagination, Scrollbar } from 'swiper';
10 | import * as SwiperReact from 'swiper/react';
11 |
12 | type SwiperProps = PropsWithChildren & SwiperReact.SwiperProps;
13 | export const Swiper = ({
14 | spaceBetween,
15 | slidesPerView,
16 | pagination,
17 | scrollbar,
18 | onSwiper,
19 | onSlideChange,
20 | allowTouchMove,
21 | children,
22 | }: SwiperProps) => {
23 | const swiperRef = useRef(null);
24 |
25 | return (
26 |
37 | {children}
38 |
39 | );
40 | };
41 |
--------------------------------------------------------------------------------
/src/components/Swiper/SwiperSlide.tsx:
--------------------------------------------------------------------------------
1 | import { PropsWithChildren } from 'react';
2 | import * as SwiperReact from 'swiper/react';
3 |
4 | type SwiperCardProps = PropsWithChildren;
5 |
6 | export const SwiperSlide = ({ children, ...rest }: SwiperCardProps) => {
7 | return {children};
8 | };
9 |
10 | SwiperSlide.displayName = 'SwiperSlide';
11 |
--------------------------------------------------------------------------------
/src/components/Swiper/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './Swiper';
2 | export * from './SwiperSlide';
3 |
--------------------------------------------------------------------------------
/src/components/Tag/Tag.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import Tag from './Tag';
4 |
5 | const meta: Meta = {
6 | title: 'components/Tag',
7 | component: Tag,
8 | };
9 |
10 | type Story = StoryObj;
11 |
12 | export const Default: Story = {
13 | args: {
14 | type: 'TRUE',
15 | label: '태그',
16 | },
17 | };
18 |
19 | export default meta;
20 |
21 | export const Tobby: Story = {
22 | args: {
23 | type: 'TOBBY',
24 | label: '태그',
25 | },
26 | };
27 |
28 | export const Buddy: Story = {
29 | args: {
30 | type: 'BUDDY',
31 | label: '태그',
32 | },
33 | };
34 |
35 | export const Pipi: Story = {
36 | args: {
37 | type: 'PIPI',
38 | label: '태그',
39 | },
40 | };
41 |
--------------------------------------------------------------------------------
/src/components/Tag/Tag.tsx:
--------------------------------------------------------------------------------
1 | import { CharacterNameModel } from '~/types/idCard';
2 |
3 | type TagProps = {
4 | type: CharacterNameModel;
5 | label: string;
6 | };
7 |
8 | const colors: Record = {
9 | BUDDY: 'text-buddy-700 border-buddy-200',
10 | TOBBY: 'text-tobby-700 border-tobby-200',
11 | PIPI: 'text-pipi-700 border-pipi-200',
12 | TRUE: 'text-true-700 border-true-200',
13 | };
14 |
15 | const getCharacterColor = (type: CharacterNameModel) => {
16 | return `${colors[type]}`;
17 | };
18 |
19 | const getClassName = (type: CharacterNameModel) => {
20 | return `${getCharacterColor(
21 | type,
22 | )} inline-block rounded-xl border border-solid px-2 py-1 text-detail font-medium bg-white`;
23 | };
24 |
25 | const Tag = ({ type, label }: TagProps) => {
26 | return {label}
;
27 | };
28 |
29 | export default Tag;
30 |
--------------------------------------------------------------------------------
/src/components/Tag/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './Tag';
2 |
--------------------------------------------------------------------------------
/src/components/Template/TemplateButton.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 |
3 | import { Button, ButtonProps } from '~/components/Button/Button';
4 | import { tw } from '~/utils/tailwind.util';
5 |
6 | type TemplateButtonProps = Partial> & {
7 | children: ReactNode | string;
8 | className?: string;
9 | };
10 |
11 | export const TemplateButton = ({ children, className, onClick, ...rest }: TemplateButtonProps) => {
12 | return (
13 |
22 | );
23 | };
24 |
--------------------------------------------------------------------------------
/src/components/Template/TemplateContent.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 |
3 | import { tw } from '~/utils/tailwind.util';
4 |
5 | type TemplateContentProps = {
6 | children?: ReactNode;
7 | className?: string;
8 | };
9 |
10 | export const TemplateContent = ({ children, className }: TemplateContentProps) => {
11 | return {children}
;
12 | };
13 |
--------------------------------------------------------------------------------
/src/components/Template/TemplateDescription.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 |
3 | type TemplateDescriptionProps = {
4 | children: ReactNode;
5 | className?: string;
6 | };
7 |
8 | export const TemplateDescription = ({ children, className }: TemplateDescriptionProps) => {
9 | return {children}
;
10 | };
11 |
--------------------------------------------------------------------------------
/src/components/Template/TemplateTitle.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 |
3 | type TemplateTitleProps = {
4 | children: ReactNode;
5 | className?: string;
6 | };
7 |
8 | export const TemplateTitle = ({ children, className }: TemplateTitleProps) => {
9 | return {children}
;
10 | };
11 |
--------------------------------------------------------------------------------
/src/components/Template/TemplateWrapper.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 |
3 | import { tw } from '~/utils/tailwind.util';
4 |
5 | type TemplateWrapperProps = {
6 | children: ReactNode;
7 | className?: string;
8 | };
9 |
10 | export const TemplateWrapper = ({ children, className }: TemplateWrapperProps) => {
11 | return {children}
;
12 | };
13 |
--------------------------------------------------------------------------------
/src/components/Template/index.ts:
--------------------------------------------------------------------------------
1 | import { TemplateButton } from './TemplateButton';
2 | import { TemplateContent } from './TemplateContent';
3 | import { TemplateDescription } from './TemplateDescription';
4 | import { TemplateTitle } from './TemplateTitle';
5 | import { TemplateWrapper } from './TemplateWrapper';
6 |
7 | export const Template = Object.assign(TemplateWrapper, {
8 | Title: TemplateTitle,
9 | Description: TemplateDescription,
10 | Content: TemplateContent,
11 | Button: TemplateButton,
12 | });
13 |
--------------------------------------------------------------------------------
/src/components/TextArea/TextAreaHeader.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { PropsWithChildren } from 'react';
4 |
5 | import { tw } from '~/utils/tailwind.util';
6 |
7 | type TextAreaHeaderProps = {
8 | className?: string;
9 | };
10 |
11 | export const TextAreaHeader = ({ className, children }: PropsWithChildren) => {
12 | return {children}
;
13 | };
14 |
--------------------------------------------------------------------------------
/src/components/TextArea/TextAreaImage.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import Image from 'next/image';
3 |
4 | import { tw } from '~/utils/tailwind.util';
5 |
6 | type SafeNumber = number | `${number}`;
7 |
8 | type TextAreaImageProps = { src: string; alt: string; height?: SafeNumber };
9 |
10 | export const TextAreaImage = ({ src, alt, height }: TextAreaImageProps) => {
11 | return (
12 |
13 |
20 |
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/src/components/TextArea/TextAreaLabel.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { PropsWithChildren } from 'react';
3 |
4 | import { ClassNameType } from '~/types/util';
5 | import { tw } from '~/utils/tailwind.util';
6 |
7 | type TextAreaLabelProps = {
8 | name: string;
9 | required?: boolean;
10 | className?: ClassNameType;
11 | };
12 |
13 | export const TextAreaLabel = ({
14 | name,
15 | required,
16 | className,
17 | children,
18 | }: PropsWithChildren) => {
19 | const requiredPseudoCss =
20 | required &&
21 | 'after:content-[" "] after:inline-block after:w-[4px] after:h-[4px] after:rounded-full after:bg-[#FF5555] after:absolute after:top-0 after:right-[-10px] ';
22 |
23 | return (
24 |
30 | );
31 | };
32 |
--------------------------------------------------------------------------------
/src/components/TextArea/TextAreaWrapper.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { ReactNode } from 'react';
3 |
4 | import { tw } from '~/utils/tailwind.util';
5 |
6 | type TextAreaWrapperProps = {
7 | children: ReactNode;
8 | className?: string;
9 | };
10 |
11 | export const TextAreaWrapper = ({ children, className }: TextAreaWrapperProps) => {
12 | return {children}
;
13 | };
14 |
--------------------------------------------------------------------------------
/src/components/TextArea/index.tsx:
--------------------------------------------------------------------------------
1 | import { TextAreaBorder } from './TextAreaBorder';
2 | import { TextAreaContent } from './TextAreaContent';
3 | import { TextAreaHeader } from './TextAreaHeader';
4 | import { TextAreaImage } from './TextAreaImage';
5 | import { TextAreaLabel } from './TextAreaLabel';
6 | import { TextAreaWrapper } from './TextAreaWrapper';
7 |
8 | export * from './useTextArea';
9 |
10 | export const TextArea = Object.assign(TextAreaWrapper, {
11 | Label: TextAreaLabel,
12 | Header: TextAreaHeader,
13 | Content: TextAreaContent,
14 | Border: TextAreaBorder,
15 | Image: TextAreaImage,
16 | });
17 |
--------------------------------------------------------------------------------
/src/components/TextArea/useAutoHeightTextArea.tsx:
--------------------------------------------------------------------------------
1 | import { RefObject, useEffect } from 'react';
2 |
3 | type UseAutoHeightTextAreaProps = {
4 | isAutoSize: boolean;
5 | ref: RefObject | null;
6 | value: string | number | readonly string[] | undefined;
7 | };
8 |
9 | export const useAutoHeightTextArea = ({ isAutoSize, ref, value }: UseAutoHeightTextAreaProps) => {
10 | useEffect(() => {
11 | if (isAutoSize && ref && ref.current) {
12 | ref.current.style.height = '0px';
13 | const scrollHeight = ref.current.scrollHeight;
14 | ref.current.style.height = scrollHeight + 'px';
15 | }
16 | }, [ref, isAutoSize, value]);
17 | };
18 |
--------------------------------------------------------------------------------
/src/components/TextArea/useTextArea.ts:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { ChangeEvent, ChangeEventHandler, useCallback, useState } from 'react';
3 |
4 | export type UseTextAreaProps = {
5 | initValue?: string;
6 | onChange: ChangeEventHandler;
7 | maxLength?: number;
8 | };
9 |
10 | export const useTextArea = ({ initValue = '', onChange, maxLength }: UseTextAreaProps) => {
11 | const [value, setValue] = useState(initValue);
12 |
13 | const onChangeHandler = useCallback(
14 | (e: ChangeEvent) => {
15 | e.target.value = e.target.value.slice(0, maxLength);
16 | onChange(e);
17 | setValue(e.target.value);
18 | },
19 | [maxLength, onChange],
20 | );
21 |
22 | return { value, onChangeHandler };
23 | };
24 |
--------------------------------------------------------------------------------
/src/components/TextInput/TextInputContent.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { forwardRef, InputHTMLAttributes } from 'react';
3 |
4 | import { ClassNameType } from '~/types/util';
5 | import { tw } from '~/utils/tailwind.util';
6 |
7 | type TextInputProps = InputHTMLAttributes & {
8 | inputClassName?: ClassNameType;
9 | };
10 |
11 | // eslint-disable-next-line react/display-name
12 | export const TextInputContent = forwardRef(
13 | (
14 | {
15 | inputClassName,
16 | name,
17 | placeholder,
18 | value,
19 | onChange,
20 | onBlur,
21 | type = 'text',
22 | disabled,
23 | ...rest
24 | },
25 | ref,
26 | ) => {
27 | const disabledCss = disabled && 'cursor-not-allowed';
28 | return (
29 |
43 | );
44 | },
45 | );
46 |
--------------------------------------------------------------------------------
/src/components/TextInput/TextInputLabel.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { PropsWithChildren } from 'react';
3 |
4 | import { ClassNameType } from '~/types/util';
5 | import { tw } from '~/utils/tailwind.util';
6 |
7 | type TextInputLabelProps = {
8 | name: string;
9 | required?: boolean;
10 | className?: ClassNameType;
11 | };
12 |
13 | export const TextInputLabel = ({
14 | name,
15 | required,
16 | className,
17 | children,
18 | }: PropsWithChildren) => {
19 | const requiredPseudoCss =
20 | required &&
21 | 'after:content-[" "] after:inline-block after:w-[4px] after:h-[4px] after:rounded-full after:bg-[#FF5555] after:absolute after:top-0 after:right-[-10px] ';
22 |
23 | return (
24 |
30 | );
31 | };
32 |
--------------------------------------------------------------------------------
/src/components/TextInput/TextInputWrapper.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { HTMLAttributes, PropsWithChildren } from 'react';
4 |
5 | import { tw } from '~/utils/tailwind.util';
6 |
7 | export const TextInputWrapper = ({
8 | className,
9 | children,
10 | ...rest
11 | }: PropsWithChildren>) => {
12 | return (
13 |
14 | {children}
15 |
16 | );
17 | };
18 |
--------------------------------------------------------------------------------
/src/components/TextInput/index.tsx:
--------------------------------------------------------------------------------
1 | import { TextInputBorder } from './TextInputBorder';
2 | import { TextInputContent } from './TextInputContent';
3 | import { TextInputLabel } from './TextInputLabel';
4 | import { TextInputWrapper } from './TextInputWrapper';
5 |
6 | export * from './useTextInput';
7 |
8 | export const TextInput = Object.assign(TextInputWrapper, {
9 | Label: TextInputLabel,
10 | Content: TextInputContent,
11 | Border: TextInputBorder,
12 | });
13 |
--------------------------------------------------------------------------------
/src/components/TextInput/useTextInput.ts:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { ChangeEvent, ChangeEventHandler, useCallback, useState } from 'react';
3 |
4 | export type UseTextInputProps = {
5 | initValue?: string;
6 | onChange: ChangeEventHandler;
7 | maxLength?: number;
8 | };
9 |
10 | export const useTextInput = ({ initValue = '', onChange, maxLength }: UseTextInputProps) => {
11 | const [value, setValue] = useState(initValue);
12 |
13 | const onChangeHandler = useCallback(
14 | (e: ChangeEvent) => {
15 | e.target.value = e.target.value.slice(0, maxLength);
16 | onChange(e);
17 | setValue(e.target.value);
18 | },
19 | [maxLength, onChange],
20 | );
21 |
22 | return { value, onChangeHandler };
23 | };
24 |
--------------------------------------------------------------------------------
/src/components/ToastMessage/ToastMessage.tsx:
--------------------------------------------------------------------------------
1 | import { ToastMessageModel, ToastMessageType } from '~/stores/toastMessage.store';
2 | import { tw } from '~/utils/tailwind.util';
3 |
4 | type ToastMessageProps = Omit;
5 |
6 | const colors: Record = {
7 | error: 'bg-grey-500 text-white',
8 | success: 'bg-grey-500 text-white',
9 | info: 'bg-grey-500 text-white',
10 | };
11 |
12 | export const ToastMessage = ({ message, type }: ToastMessageProps) => {
13 | return (
14 |
21 | {message}
22 |
23 | );
24 | };
25 |
--------------------------------------------------------------------------------
/src/components/ToastMessage/ToastMessageProvider.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { AnimatePresence, motion } from 'framer-motion';
3 |
4 | import { Portal } from '~/components/Portal';
5 | import { ToastMessage } from '~/components/ToastMessage/ToastMessage';
6 | import { useToastMessageStore } from '~/stores/toastMessage.store';
7 |
8 | export const ToastMessageProvider = () => {
9 | const { toastMessageList } = useToastMessageStore();
10 | return (
11 |
12 |
13 |
14 |
15 | {toastMessageList.map(({ toastId, message, type }) => (
16 |
23 |
24 |
25 | ))}
26 |
27 |
28 |
29 |
30 | );
31 | };
32 |
--------------------------------------------------------------------------------
/src/components/ToastMessage/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ToastMessage';
2 | export * from './ToastMessageProvider';
3 |
--------------------------------------------------------------------------------
/src/components/TopNavigation/TopNavigationLeft.tsx:
--------------------------------------------------------------------------------
1 | import { PropsWithChildren } from 'react';
2 |
3 | import { tw } from '~/utils/tailwind.util';
4 |
5 | type TopNavigationLeftProps = {
6 | className?: string;
7 | };
8 |
9 | export const TopNavigationLeft = ({
10 | children,
11 | className,
12 | }: PropsWithChildren) => {
13 | return (
14 | {children}
15 | );
16 | };
17 |
--------------------------------------------------------------------------------
/src/components/TopNavigation/TopNavigationProgressBar.tsx:
--------------------------------------------------------------------------------
1 | import { ProgressBar } from '../ProgressBar';
2 |
3 | type TopNavigationProgressBarProps = {
4 | currentStep: number;
5 | stepsLength: number;
6 | };
7 |
8 | export const TopNavigationProgressBar = ({
9 | currentStep,
10 | stepsLength,
11 | }: TopNavigationProgressBarProps) => {
12 | return (
13 |
16 | );
17 | };
18 |
--------------------------------------------------------------------------------
/src/components/TopNavigation/TopNavigationRight.tsx:
--------------------------------------------------------------------------------
1 | import { PropsWithChildren } from 'react';
2 |
3 | import { tw } from '~/utils/tailwind.util';
4 |
5 | type TopNavigationRightProps = {
6 | className?: string;
7 | };
8 |
9 | export const TopNavigationRight = ({
10 | children,
11 | className,
12 | }: PropsWithChildren) => {
13 | return {children}
;
14 | };
15 |
--------------------------------------------------------------------------------
/src/components/TopNavigation/TopNavigationTitle.tsx:
--------------------------------------------------------------------------------
1 | import { PropsWithChildren } from 'react';
2 |
3 | import { tw } from '~/utils/tailwind.util';
4 |
5 | type TopNavigationTitleProps = {
6 | className?: string;
7 | };
8 |
9 | export const TopNavigationTitle = ({
10 | children,
11 | className,
12 | }: PropsWithChildren) => {
13 | return {children}
;
14 | };
15 |
--------------------------------------------------------------------------------
/src/components/TopNavigation/TopNavigationWrapper.tsx:
--------------------------------------------------------------------------------
1 | import { PropsWithChildren } from 'react';
2 |
3 | type TopNavigationWrapperProps = {
4 | /**
5 | * border-bottom 컬러 값이 주어지면 표시합니다. ex) color-primary
6 | */
7 | bottomBorderColor?: string;
8 | bgColor?: string;
9 | };
10 | export const TopNavigationWrapper = ({
11 | bottomBorderColor,
12 | bgColor = 'bg-white',
13 | children,
14 | }: PropsWithChildren) => {
15 | const borderBottomStyle = bottomBorderColor ? `border-b-${bottomBorderColor} border-b-[1px]` : '';
16 | return (
17 |
22 | );
23 | };
24 |
--------------------------------------------------------------------------------
/src/components/TopNavigation/index.tsx:
--------------------------------------------------------------------------------
1 | import { TopNavigationBackButton } from './TopNavigationBackButton';
2 | import { TopNavigationLeft } from './TopNavigationLeft';
3 | import { TopNavigationProgressBar } from './TopNavigationProgressBar';
4 | import { TopNavigationRight } from './TopNavigationRight';
5 | import { TopNavigationTitle } from './TopNavigationTitle';
6 | import { TopNavigationWrapper } from './TopNavigationWrapper';
7 |
8 | export const TopNavigation = Object.assign(TopNavigationWrapper, {
9 | Left: TopNavigationLeft,
10 | Title: TopNavigationTitle,
11 | Right: TopNavigationRight,
12 | BackButton: TopNavigationBackButton,
13 | ProgressBar: TopNavigationProgressBar,
14 | });
15 |
--------------------------------------------------------------------------------
/src/constant/planet.ts:
--------------------------------------------------------------------------------
1 | export const DEFAULT_SELECT_PLANET_INDEX = 0;
2 |
--------------------------------------------------------------------------------
/src/constant/recommendKeyword.ts:
--------------------------------------------------------------------------------
1 | import { OptionType } from '~/components/KeywordInput';
2 |
3 | export const DEFAULT_RECOMMEND_KEYWORD_OPTIONS: OptionType[] = [
4 | {
5 | title: '밤 산책',
6 | imageUrl: '',
7 | content: '',
8 | },
9 | {
10 | title: '아메리카노',
11 | imageUrl: '',
12 | content: '',
13 | },
14 | {
15 | title: '해외 축구',
16 | imageUrl: '',
17 | content: '',
18 | },
19 | {
20 | title: '음악',
21 | imageUrl: '',
22 | content: '',
23 | },
24 | {
25 | title: '공포영화',
26 | imageUrl: '',
27 | content: '',
28 | },
29 | {
30 | title: '셀프 인테리어',
31 | imageUrl: '',
32 | content: '',
33 | },
34 | {
35 | title: '엽떡',
36 | imageUrl: '',
37 | content: '',
38 | },
39 | {
40 | title: '우주 맛집 투어',
41 | imageUrl: '',
42 | content: '',
43 | },
44 | {
45 | title: '필름카메라',
46 | imageUrl: '',
47 | content: '',
48 | },
49 | {
50 | title: '강아지 영상보기',
51 | imageUrl: '',
52 | content: '',
53 | },
54 | {
55 | title: '오버워치',
56 | imageUrl: '',
57 | content: '',
58 | },
59 | ];
60 |
--------------------------------------------------------------------------------
/src/hooks/useIsMounted.hooks.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 |
3 | function useIsMounted() {
4 | const [isMounted, setIsMounted] = useState(false);
5 |
6 | useEffect(() => {
7 | setIsMounted(true);
8 | }, []);
9 |
10 | return isMounted;
11 | }
12 |
13 | export default useIsMounted;
14 |
--------------------------------------------------------------------------------
/src/hooks/usePlanetNavigate.ts:
--------------------------------------------------------------------------------
1 | export const usePlanetNavigate = () => {
2 | const extractPlanetIdFromPathname = (currentPathname: string) => {
3 | const parts = currentPathname.split('/');
4 | const planetIdIndex = parts.findIndex(part => part === 'planet' || part === 'my-page');
5 |
6 | const notFoundPlanetID = planetIdIndex === -1;
7 | const planetIdIndexOverPathLength = planetIdIndex + 1 >= parts.length;
8 |
9 | if (notFoundPlanetID) return null;
10 | if (planetIdIndexOverPathLength) return null;
11 |
12 | return Number(parts[planetIdIndex + 1]);
13 | };
14 | return {
15 | extractPlanetIdFromPathname,
16 | };
17 | };
18 |
--------------------------------------------------------------------------------
/src/lib/tanstackQuery/getQueryClient.tsx:
--------------------------------------------------------------------------------
1 | import { QueryClient } from '@tanstack/react-query';
2 | import { cache } from 'react';
3 |
4 | const getQueryClient = cache(() => new QueryClient());
5 | export default getQueryClient;
6 |
--------------------------------------------------------------------------------
/src/mocks/browser.ts:
--------------------------------------------------------------------------------
1 | import { setupWorker } from 'msw';
2 |
3 | import { handlers } from './handlers';
4 |
5 | export const worker = setupWorker(...handlers);
6 |
--------------------------------------------------------------------------------
/src/mocks/handlers.ts:
--------------------------------------------------------------------------------
1 | import { commentMockHandler } from '~/mocks/comment/comment.mockHandler';
2 | import { communityMockHandler } from '~/mocks/community/community.mockHandler';
3 | import { idCardMockHandler } from '~/mocks/idCard/idCard.mockHandler';
4 | import { notificationMockHandler } from '~/mocks/notification/notification.mockHandler';
5 | import { nudgeMockHandler } from '~/mocks/nudge/nudge.mockHandler';
6 | import { userMockHandler } from '~/mocks/user/user.mockHandler';
7 |
8 | export const handlers = [
9 | ...idCardMockHandler,
10 | ...communityMockHandler,
11 | ...commentMockHandler,
12 | ...notificationMockHandler,
13 | ...userMockHandler,
14 | ...nudgeMockHandler,
15 | ];
16 |
--------------------------------------------------------------------------------
/src/mocks/idCard/idCard.mock.ts:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker/locale/ko';
2 |
3 | import { generateRandomNudge } from '~/mocks/nudge.util';
4 | import { IdCardCreateResponse, IdCardDetailModel } from '~/types/idCard';
5 |
6 | export const idCardDetailMock = (): IdCardDetailModel => ({
7 | idCardId: faker.number.int(),
8 | userId: faker.number.int(),
9 | nickname: faker.person.fullName(),
10 | profileImageUrl: faker.image.avatar(),
11 | aboutMe: faker.lorem.paragraph(),
12 | keywords: Array.from({ length: 3 }, () => ({
13 | keywordId: faker.number.int(),
14 | title: faker.lorem.word(),
15 | imageUrl: faker.image.avatar(),
16 | content: faker.lorem.paragraph(),
17 | })),
18 | characterType: faker.helpers.arrayElement(['TRUE', 'PIPI', 'TOBBY', 'BUDDY']),
19 | commentCount: faker.number.int({ min: 0, max: 999 }),
20 | toNudgeType: generateRandomNudge(),
21 | unreadNudges: faker.number.int({ min: 0, max: 999 }),
22 | });
23 |
24 | export const createIdCardMock: IdCardCreateResponse = { id: 1 };
25 |
--------------------------------------------------------------------------------
/src/mocks/idCard/idCard.mockHandler.ts:
--------------------------------------------------------------------------------
1 | import { rest } from 'msw';
2 |
3 | import { ROOT_API_URL } from '~/api/config/requestUrl';
4 | import { createIdCardMock, idCardDetailMock } from '~/mocks/idCard/idCard.mock';
5 | import { generateResponse } from '~/mocks/mock.util';
6 |
7 | export const idCardMockHandler = [
8 | rest.get(`${ROOT_API_URL}/id-cards/:idCardId`, () => {
9 | return generateResponse({ statusCode: 200, data: { idCardDetailsDto: idCardDetailMock() } });
10 | }),
11 | rest.get(`${ROOT_API_URL}/communities/:communityId/users/idCards`, () => {
12 | return generateResponse({ statusCode: 200, data: { idCardDetailsDto: idCardDetailMock() } });
13 | }),
14 | rest.put(`${ROOT_API_URL}/id-cards/:idCardId`, () => {
15 | return generateResponse({ statusCode: 200, data: idCardDetailMock() });
16 | }),
17 | rest.post(`${ROOT_API_URL}/id-cards`, () => {
18 | return generateResponse({ statusCode: 200, data: createIdCardMock });
19 | }),
20 | ];
21 |
--------------------------------------------------------------------------------
/src/mocks/image/image.mock.ts:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker/locale/ko';
2 |
3 | export const imageUrlMock = {
4 | imageUrl: faker.image.avatar(),
5 | };
6 |
--------------------------------------------------------------------------------
/src/mocks/image/image.mockHandler.ts:
--------------------------------------------------------------------------------
1 | import { rest } from 'msw';
2 |
3 | import { ROOT_API_URL } from '~/api/config/requestUrl';
4 | import { generateResponse } from '~/mocks/mock.util';
5 |
6 | import { imageUrlMock } from './image.mock';
7 |
8 | export const imageMockHandler = [
9 | rest.post(`${ROOT_API_URL}/images`, () => {
10 | return generateResponse({ statusCode: 200, data: imageUrlMock });
11 | }),
12 | ];
13 |
--------------------------------------------------------------------------------
/src/mocks/index.ts:
--------------------------------------------------------------------------------
1 | const initMocks = async () => {
2 | const isServer = typeof window === 'undefined';
3 |
4 | if (isServer) {
5 | const { server } = await require('./server');
6 | server.listen({ onUnhandledRequest: 'bypass' });
7 | } else {
8 | const { worker } = await require('./browser');
9 | worker.start({ onUnhandledRequest: 'bypass' });
10 | }
11 | };
12 |
13 | export default initMocks;
14 |
--------------------------------------------------------------------------------
/src/mocks/mock.util.ts:
--------------------------------------------------------------------------------
1 | import { context, response } from 'msw';
2 |
3 | import { DefaultServerResponseType } from '~/api/config/api.types';
4 |
5 | export const generateResponse = ({
6 | data,
7 | statusCode,
8 | delay = 0,
9 | }: Omit, 'success'> & { delay?: number }) => {
10 | const isSuccess = statusCode < 400;
11 | return response(
12 | context.status(statusCode),
13 | context.delay(delay),
14 | context.json({
15 | data,
16 | statusCode,
17 | success: isSuccess,
18 | }),
19 | );
20 | };
21 |
--------------------------------------------------------------------------------
/src/mocks/notification/notification.mockHandler.ts:
--------------------------------------------------------------------------------
1 | import { rest } from 'msw';
2 |
3 | import { ROOT_API_URL } from '~/api/config/requestUrl';
4 | import { generateResponse } from '~/mocks/mock.util';
5 |
6 | import { createNotificationList } from './notification.mock';
7 |
8 | export const notificationMockHandler = [
9 | rest.get(`${ROOT_API_URL}/notifications?page=:page&size=10`, req => {
10 | const { searchParams } = req.url;
11 | const page = Number(searchParams.get('page'));
12 | return generateResponse({
13 | statusCode: 200,
14 | data: createNotificationList(10, page, 10),
15 | });
16 | }),
17 | ];
18 |
--------------------------------------------------------------------------------
/src/mocks/nudge.util.ts:
--------------------------------------------------------------------------------
1 | import { NudgeModel } from '~/types/nudge';
2 |
3 | const keywords: NudgeModel[] = ['MEET', 'SIMILARITY', 'TALKING', 'FRIENDLY'];
4 |
5 | export const generateRandomNudge = () => {
6 | const randomIndex = Math.floor(Math.random() * keywords.length);
7 | const keyword = keywords[randomIndex];
8 |
9 | return keyword;
10 | };
11 |
--------------------------------------------------------------------------------
/src/mocks/nudge/nudge.mock.ts:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker/locale/ko';
2 |
3 | import { NudgeListModel } from '~/types/nudge/model.type';
4 |
5 | export const createNudgeList = (): NudgeListModel[] => {
6 | return Array.from({ length: 5 }, () => ({
7 | nudgeId: faker.number.int(),
8 | opponentUser: {
9 | userId: faker.number.int(),
10 | profileImageUrl: faker.image.avatar(),
11 | nickname: faker.person.fullName(),
12 | },
13 | toUserNudgeType: faker.helpers.arrayElement(['FRIENDLY', 'SIMILARITY', 'TALKING', 'MEET']),
14 | fromUserNudgeType: faker.helpers.arrayElement([
15 | 'FRIENDLY',
16 | 'SIMILARITY',
17 | 'TALKING',
18 | 'MEET',
19 | null,
20 | ]),
21 | }));
22 | };
23 |
--------------------------------------------------------------------------------
/src/mocks/nudge/nudge.mockHandler.ts:
--------------------------------------------------------------------------------
1 | import { rest } from 'msw';
2 |
3 | import { ROOT_API_URL } from '~/api/config/requestUrl';
4 | import { generateResponse } from '~/mocks/mock.util';
5 | import { createNudgeList } from '~/mocks/nudge/nudge.mock';
6 |
7 | export const nudgeMockHandler = [
8 | rest.get(`${ROOT_API_URL}/nudges/id-cards/:idCardsId`, () => {
9 | return generateResponse({ statusCode: 200, data: { nudgeInfoDtos: createNudgeList() } });
10 | }),
11 | ];
12 |
--------------------------------------------------------------------------------
/src/mocks/server.ts:
--------------------------------------------------------------------------------
1 | import { setupServer } from 'msw/node';
2 |
3 | import { handlers } from './handlers';
4 |
5 | export const server = setupServer(...handlers);
6 |
--------------------------------------------------------------------------------
/src/mocks/user/user.mock.ts:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker/locale/ko';
2 |
3 | import { UserInfoModel } from '~/types/user';
4 |
5 | export const createUserInfo = (): UserInfoModel => ({
6 | userId: faker.number.int(),
7 | email: faker.internet.email(),
8 | nickname: faker.person.fullName(),
9 | gender: faker.person.gender(),
10 | ageRange: '',
11 | profileImageUrl: faker.image.avatar(),
12 | characterType: 'BUDDY',
13 | communityIds: [1],
14 | });
15 |
--------------------------------------------------------------------------------
/src/mocks/user/user.mockHandler.ts:
--------------------------------------------------------------------------------
1 | import { rest } from 'msw';
2 |
3 | import { ROOT_API_URL } from '~/api/config/requestUrl';
4 | import { generateResponse } from '~/mocks/mock.util';
5 | import { createUserInfo } from '~/mocks/user/user.mock';
6 |
7 | export const userMockHandler = [
8 | rest.get(`${ROOT_API_URL}/user/profile`, () => {
9 | return generateResponse({
10 | statusCode: 200,
11 | data: {
12 | userProfileDto: createUserInfo(),
13 | },
14 | });
15 | }),
16 | rest.post(`${ROOT_API_URL}/user/character`, () => {
17 | return generateResponse({ statusCode: 200, data: {} });
18 | }),
19 | ];
20 |
--------------------------------------------------------------------------------
/src/modules/CommentInput/CommentInput.client.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useGetCommunityUserInfo } from '~/api/domain/community.api';
4 | import { ActiveCommentInput } from '~/modules/CommentInput/ActiveCommentInput.client';
5 | import { DisabledCommentInput } from '~/modules/CommentInput/DisabledCommentInput.client';
6 |
7 | type CommentInputProps = {
8 | idCardId: number;
9 | communityId: number;
10 | };
11 |
12 | export const CommentInput = ({ idCardId, communityId }: CommentInputProps) => {
13 | const { data } = useGetCommunityUserInfo(communityId);
14 |
15 | const shouldActiveCommentInput = data?.myInfoInInCommunityDto.isExistsIdCard;
16 |
17 | return (
18 |
19 | {shouldActiveCommentInput ? (
20 |
25 | ) : (
26 |
27 | )}
28 |
29 | );
30 | };
31 |
--------------------------------------------------------------------------------
/src/modules/CommentInput/CommentInput.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { CommentInput } from '~/modules/CommentInput/CommentInput.client';
4 |
5 | const meta: Meta = {
6 | title: 'modules/CommentInput',
7 | component: CommentInput,
8 | };
9 |
10 | type Story = StoryObj;
11 |
12 | export const Default: Story = {
13 | render: () => ,
14 | };
15 |
16 | export default meta;
17 |
--------------------------------------------------------------------------------
/src/modules/CommentInput/ReplyIndicator.client.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React, { useEffect } from 'react';
4 |
5 | import { Divider } from '~/components/Divider';
6 | import { CancelIcon } from '~/components/Icon';
7 | import { useReplyRecipientStore } from '~/stores/comment.store';
8 |
9 | export const ReplyIndicator = () => {
10 | const { nickname, clear } = useReplyRecipientStore();
11 |
12 | useEffect(() => {
13 | return () => {
14 | clear();
15 | };
16 | }, []);
17 |
18 | return nickname ? (
19 | <>
20 |
21 |
22 | {nickname}님에게 답글 남기는 중
23 |
26 |
27 | >
28 | ) : (
29 | <>>
30 | );
31 | };
32 |
--------------------------------------------------------------------------------
/src/modules/CommentInput/index.ts:
--------------------------------------------------------------------------------
1 | export * from './CommentInput.client';
2 |
--------------------------------------------------------------------------------
/src/modules/CommentList/Comment/Comment.stories.tsx:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker/locale/ko';
2 | import type { Meta, StoryObj } from '@storybook/react';
3 |
4 | import { createComment } from '~/mocks/comment/comment.mock';
5 |
6 | import { Comment } from './index';
7 |
8 | const meta: Meta = {
9 | title: 'modules/Comment',
10 | component: Comment,
11 | args: {},
12 | };
13 |
14 | export default meta;
15 |
16 | type Story = StoryObj;
17 |
18 | const MOCK_COMMENT = createComment(123123, 123);
19 |
20 | export const Primary: Story = {
21 | render: () => ,
22 | };
23 |
24 | export const DetailShow: Story = {
25 | render: () => ,
26 | };
27 |
--------------------------------------------------------------------------------
/src/modules/CommentList/Comment/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Comment.client';
2 |
--------------------------------------------------------------------------------
/src/modules/CommentList/CommentCommon/CommentOptions.client.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useEffect, useState } from 'react';
4 |
5 | import { DeleteButton } from '~/modules/CommentList/CommentCommon/DeleteButton.client';
6 | import { ReportButton } from '~/modules/CommentList/CommentCommon/ReportButton.client';
7 | import { CommentModel } from '~/types/comment';
8 | import { getUserIdClient } from '~/utils/auth/getUserId.client';
9 |
10 | type CommentOptionsProps = Pick & {
11 | onClickToDeleteComment: VoidFunction;
12 | };
13 |
14 | export const CommentOptions = ({ writerInfo, onClickToDeleteComment }: CommentOptionsProps) => {
15 | const { userId: writerId } = writerInfo;
16 | const userId = getUserIdClient();
17 |
18 | const [isMine, setIsMine] = useState(false);
19 |
20 | const isWriterSameAsUser = userId === writerId;
21 |
22 | // Text content does not match server-rendered HTML 이슈로 useEffect로 분리처리
23 | useEffect(() => {
24 | setIsMine(isWriterSameAsUser);
25 | }, [isWriterSameAsUser, userId, writerId]);
26 |
27 | return (
28 | <>
29 | {isMine ? : }
30 | >
31 | );
32 | };
33 |
--------------------------------------------------------------------------------
/src/modules/CommentList/CommentCommon/DeleteButton.client.tsx:
--------------------------------------------------------------------------------
1 | import { SimpleConfirmPopup, useConfirmPopup } from '~/components/ConfirmPopup';
2 |
3 | type DeleteButtonProps = {
4 | onClickToDeleteComment: () => void;
5 | };
6 |
7 | export const DeleteButton = ({ onClickToDeleteComment }: DeleteButtonProps) => {
8 | const { isOpen, openPopup, closePopup, confirm } = useConfirmPopup();
9 |
10 | const deleteComment = () => {
11 | onClickToDeleteComment();
12 | };
13 |
14 | const onDeleteComment = async () => {
15 | const isOk = await openPopup();
16 | closePopup();
17 | if (isOk) {
18 | deleteComment();
19 | }
20 | };
21 |
22 | return (
23 | <>
24 |
27 | {isOpen && (
28 |
35 | )}
36 | >
37 | );
38 | };
39 |
--------------------------------------------------------------------------------
/src/modules/CommentList/CommentCommon/Empty.client.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image';
2 |
3 | export const Empty = () => {
4 | return (
5 |
6 |
7 | 가장 먼저 댓글을 남겨보세요
8 |
9 | );
10 | };
11 |
--------------------------------------------------------------------------------
/src/modules/CommentList/CommentCommon/Header.client.tsx:
--------------------------------------------------------------------------------
1 | import { CommentModel, CommentWriterIntoModel } from '~/types/comment';
2 | import { getCreatedAtFormat } from '~/utils/time.util';
3 |
4 | type HeaderProps = Pick & Pick;
5 |
6 | export const Header = ({ nickname, createdAt }: HeaderProps) => {
7 | return (
8 |
9 | {nickname}
10 | {getCreatedAtFormat(createdAt)}전
11 |
12 | );
13 | };
14 |
--------------------------------------------------------------------------------
/src/modules/CommentList/CommentCommon/LikeCount.client.tsx:
--------------------------------------------------------------------------------
1 | import { CommentLikeModel } from '~/types/comment';
2 |
3 | type LikeCountProps = Pick;
4 |
5 | export const LikeCount = ({ likeCount }: LikeCountProps) => {
6 | const isShowLikeText = likeCount !== 0;
7 | return (
8 | <>
9 | {isShowLikeText && (
10 | 좋아요 {likeCount}개
11 | )}
12 | >
13 | );
14 | };
15 |
--------------------------------------------------------------------------------
/src/modules/CommentList/CommentCommon/LikeIcon.client.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { HeartFillIcon, HeartIcon } from '~/components/Icon';
4 | import { CommentLikeModel } from '~/types/comment';
5 |
6 | type LikeIconProps = Pick & {
7 | onClickToLikeCancel: () => Promise;
8 | onClickToLike: () => Promise;
9 | };
10 |
11 | export const LikeIcon = ({
12 | likedByCurrentUser,
13 | onClickToLikeCancel,
14 | onClickToLike,
15 | }: LikeIconProps) => {
16 | return (
17 | <>
18 | {likedByCurrentUser ? (
19 |
20 | ) : (
21 |
22 | )}
23 | >
24 | );
25 | };
26 |
--------------------------------------------------------------------------------
/src/modules/CommentList/CommentCommon/ReplyHideButton.client.tsx:
--------------------------------------------------------------------------------
1 | import { DashIcon } from '~/components/Icon';
2 |
3 | type ReplyHideButtonProps = {
4 | isShowReplyList: boolean;
5 | onClickHideReplyList: VoidFunction;
6 | };
7 |
8 | export const ReplyHideButton = ({
9 | isShowReplyList,
10 | onClickHideReplyList,
11 | }: ReplyHideButtonProps) => {
12 | return (
13 | <>
14 | {isShowReplyList && (
15 |
19 | )}
20 | >
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/src/modules/CommentList/CommentCommon/ReplyShowButton.client.tsx:
--------------------------------------------------------------------------------
1 | import { DashIcon } from '~/components/Icon';
2 | import { CommentModel } from '~/types/comment';
3 |
4 | type ReplyShowButtonProps = Pick & {
5 | isShowReplyList: boolean;
6 | onClickShowReplyList: VoidFunction;
7 | };
8 |
9 | export const ReplyShowButton = ({
10 | isShowReplyList,
11 | onClickShowReplyList,
12 | repliesCount,
13 | }: ReplyShowButtonProps) => {
14 | const isReplyListEmpty = repliesCount === 0;
15 |
16 | const isShowButton = !isShowReplyList && !isReplyListEmpty;
17 |
18 | return (
19 | <>
20 | {isShowButton && (
21 |
31 | )}
32 | >
33 | );
34 | };
35 |
--------------------------------------------------------------------------------
/src/modules/CommentList/CommentCommon/ReplySubmitButton.client.tsx:
--------------------------------------------------------------------------------
1 | import { useReplyRecipientStore } from '~/stores/comment.store';
2 |
3 | type ReplySubmitButtonProps = {
4 | nickname: string;
5 | commentId: number;
6 | };
7 |
8 | export const ReplySubmitButton = ({ nickname, commentId }: ReplySubmitButtonProps) => {
9 | const { setReplyRecipient } = useReplyRecipientStore();
10 | return (
11 |
17 | );
18 | };
19 |
--------------------------------------------------------------------------------
/src/modules/CommentList/CommentCommon/ReportButton.client.tsx:
--------------------------------------------------------------------------------
1 | import { SimpleConfirmPopup, useConfirmPopup } from '~/components/ConfirmPopup';
2 | import { useToastMessageStore } from '~/stores/toastMessage.store';
3 |
4 | export const ReportButton = () => {
5 | const { isOpen, openPopup, closePopup, confirm } = useConfirmPopup();
6 | const { infoToast } = useToastMessageStore();
7 | const handleReport = async () => {
8 | const isOk = await openPopup();
9 | closePopup();
10 | if (isOk) {
11 | infoToast('신고가 접수됐습니다');
12 | }
13 | };
14 |
15 | return (
16 | <>
17 |
20 | {isOpen && (
21 |
28 | )}
29 | >
30 | );
31 | };
32 |
--------------------------------------------------------------------------------
/src/modules/CommentList/CommentCommon/UserProfile.client.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image';
2 |
3 | import { CommentWriterIntoModel } from '~/types/comment';
4 |
5 | type UserProfileProps = Pick;
6 |
7 | export const UserProfile = ({ profileImageUrl }: UserProfileProps) => {
8 | return (
9 |
10 |
17 |
18 | );
19 | };
20 |
--------------------------------------------------------------------------------
/src/modules/CommentList/CommentCommon/index.ts:
--------------------------------------------------------------------------------
1 | export * from './CommentOptions.client';
2 | export * from './Content.client';
3 | export * from './DeleteButton.client';
4 | export * from './Empty.client';
5 | export * from './Header.client';
6 | export * from './LikeCount.client';
7 | export * from './LikeIcon.client';
8 | export * from './ReplyHideButton.client';
9 | export * from './ReplyShowButton.client';
10 | export * from './ReplySubmitButton.client';
11 | export * from './ReportButton.client';
12 | export * from './UserProfile.client';
13 |
--------------------------------------------------------------------------------
/src/modules/CommentList/CommentReplyList/index.ts:
--------------------------------------------------------------------------------
1 | export * from './CommentReply.client';
2 | export * from './CommentReplyList.client';
3 |
--------------------------------------------------------------------------------
/src/modules/CommentList/useLike.ts:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 |
3 | import { CommentLikeModel } from '~/types/comment';
4 |
5 | type UseLikeProps = CommentLikeModel;
6 |
7 | export const useLike = ({
8 | likedByCurrentUser: initLikedByCurrentUser,
9 | likeCount: initLikeCount,
10 | }: UseLikeProps) => {
11 | const [likedByCurrentUser, setLikedByCurrentUser] = useState(initLikedByCurrentUser);
12 | const [likeCount, setLikeCount] = useState(initLikeCount);
13 |
14 | const likeComment = () => {
15 | setLikedByCurrentUser(true);
16 | setLikeCount(prev => prev + 1);
17 | };
18 |
19 | const cancelLikeComment = () => {
20 | setLikedByCurrentUser(false);
21 | setLikeCount(prev => (prev === 0 ? prev : prev - 1));
22 | };
23 |
24 | return {
25 | likedByCurrentUser,
26 | likeCount,
27 | likeComment,
28 | cancelLikeComment,
29 | };
30 | };
31 |
--------------------------------------------------------------------------------
/src/modules/CommunityAdmin/CommunityAdmin.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { CommunityAdmin } from './CommunityAdmin';
4 | import { CommunityAdminCreate } from './CommunityAdminCreate.client';
5 | import { CommunityAdminEdit } from './CommunityAdminEdit.client';
6 |
7 | const meta: Meta = {
8 | title: 'modules/CommunityAdmin',
9 | component: CommunityAdmin,
10 | };
11 |
12 | type Story = StoryObj;
13 |
14 | export const Default: Story = {
15 | render: () => ,
16 | };
17 |
18 | export default meta;
19 |
20 | export const Create: StoryObj = {
21 | render: () => ,
22 | };
23 |
24 | export const Edit: StoryObj = {
25 | render: () => ,
26 | };
27 |
--------------------------------------------------------------------------------
/src/modules/CommunityAdmin/CommunityAdmin.type.ts:
--------------------------------------------------------------------------------
1 | export type DuplicateState = 'DEFAULT' | 'SUCCESS' | 'ERROR';
2 |
--------------------------------------------------------------------------------
/src/modules/CommunityProfile/CommunityLogoImage.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image';
2 | import { twMerge } from 'tailwind-merge';
3 |
4 | type LogoSize = 'small' | 'medium' | 'large';
5 | type CommunityLogoImageProps = {
6 | logoImageUrl?: string;
7 | size?: LogoSize;
8 | };
9 |
10 | const sizes: Record = {
11 | small: 'h-20pxr w-20pxr',
12 | medium: 'h-32pxr w-32pxr',
13 | large: 'h-60pxr w-60pxr',
14 | };
15 | export const CommunityLogoImage = ({ logoImageUrl, size = 'large' }: CommunityLogoImageProps) => {
16 | const logoSize = sizes[size];
17 | const defaultPlanetLogoImage = logoImageUrl || '/assets/images/default_planet_logo.png';
18 |
19 | return (
20 |
21 |
27 |
28 | );
29 | };
30 |
--------------------------------------------------------------------------------
/src/modules/CommunityProfile/CommunityProfile.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { CommunityBgImage } from './CommunityBgImage.client';
4 | import { CommunityProfile } from './CommunityProfile';
5 |
6 | const meta: Meta = {
7 | title: 'modules/CommunityProfile',
8 | component: CommunityProfile,
9 | };
10 |
11 | export default meta;
12 |
13 | type Story = StoryObj;
14 | export const CommunityProfileStory: Story = {
15 | render: () => (
16 |
21 | ),
22 | };
23 |
24 | export const CommunityBgImageStory: StoryObj = {
25 | render: () => (
26 |
37 | ),
38 | };
39 |
--------------------------------------------------------------------------------
/src/modules/CommunityProfile/CommunityProfile.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 |
3 | import { CommunityDetailModel } from '~/types/community';
4 |
5 | import { CommunityLogoImage } from './CommunityLogoImage';
6 |
7 | type CommunityProfileProps = Pick<
8 | CommunityDetailModel,
9 | 'logoImageUrl' | 'description' | 'userCount'
10 | > & { top?: ReactNode };
11 |
12 | export const CommunityProfile = ({
13 | logoImageUrl,
14 | description,
15 | userCount,
16 | top,
17 | }: CommunityProfileProps) => {
18 | return (
19 |
20 | {top}
21 |
22 |
23 |
24 |
{`주민 ${userCount}명`}
25 |
{description}
26 |
27 |
28 |
29 | );
30 | };
31 |
--------------------------------------------------------------------------------
/src/modules/CommunityProfile/index.ts:
--------------------------------------------------------------------------------
1 | export * from './CommunityBgImage.client';
2 | export * from './CommunityLogoImage';
3 | export * from './CommunityProfile';
4 |
--------------------------------------------------------------------------------
/src/modules/CreateIdCardButton/CreateIdCardButton.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { CreateIdCardButton } from './index';
4 |
5 | const meta: Meta = {
6 | title: 'modules/CreateIdCardButton',
7 | component: CreateIdCardButton,
8 | args: {},
9 | };
10 |
11 | export default meta;
12 |
13 | type Story = StoryObj;
14 |
15 | export const Primary: Story = {
16 | render: () => ,
17 | };
18 |
--------------------------------------------------------------------------------
/src/modules/CreateIdCardButton/index.ts:
--------------------------------------------------------------------------------
1 | export * from './CreateIdCardButton.client';
2 |
--------------------------------------------------------------------------------
/src/modules/IdCard/index.ts:
--------------------------------------------------------------------------------
1 | export * from './IdCard.client';
2 |
--------------------------------------------------------------------------------
/src/modules/IdCardCreation/Form/index.ts:
--------------------------------------------------------------------------------
1 | export * from './IdCardCreationForm';
2 |
--------------------------------------------------------------------------------
/src/modules/IdCardCreation/IdCardCreation.type.ts:
--------------------------------------------------------------------------------
1 | export type CreationSteps = 'BOARDING' | 'PROFILE' | 'KEYWORD' | 'KEYWORD_CONTENT' | 'COMPLETE';
2 |
--------------------------------------------------------------------------------
/src/modules/IdCardCreation/Step/KeywordContentImage.client.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useCallback } from 'react';
4 | import { useFormContext } from 'react-hook-form';
5 |
6 | import { CancelCircleIcon } from '~/components/Icon';
7 | import { IdCardCreationFormModel } from '~/types/idCard';
8 | type KeywordContentImageProps = {
9 | index: number;
10 | };
11 |
12 | export const KeywordContentImage = ({ index }: KeywordContentImageProps) => {
13 | const { watch, setValue } = useFormContext();
14 | const { keywords } = watch();
15 | const imageUrl = keywords[index].imageUrl;
16 |
17 | const onCancelClick = useCallback(() => {
18 | setValue(`keywords.${index}.imageUrl`, '');
19 | }, [index, setValue]);
20 |
21 | return imageUrl ? (
22 |
23 | {/* eslint-disable-next-line @next/next/no-img-element */}
24 |

25 |
26 |
27 |
28 |
29 | ) : null;
30 | };
31 |
--------------------------------------------------------------------------------
/src/modules/IdCardCreation/Step/KeywordContentStep.client.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { useFormContext } from 'react-hook-form';
3 |
4 | import { KeywordContentEditCard } from '~/modules/KeywordContentEditCard';
5 | import { FormKeywordModel, IdCardCreationFormModel } from '~/types/idCard';
6 |
7 | const title = '나를 소개하는 키워드의\n 설명을 적어주세요!';
8 |
9 | export const KeywordContentStep = () => {
10 | const { getValues } = useFormContext();
11 | const keywords = getValues('keywords');
12 |
13 | return (
14 |
15 |
{title}
16 |
17 | {keywords.map((keyword: FormKeywordModel, index: number) => {
18 | return ;
19 | })}
20 |
21 |
22 | );
23 | };
24 |
--------------------------------------------------------------------------------
/src/modules/IdCardCreation/Step/KeywordStep.client.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { Controller, useFormContext } from 'react-hook-form';
4 |
5 | import { KeywordInput } from '~/components/KeywordInput';
6 | import { DEFAULT_RECOMMEND_KEYWORD_OPTIONS } from '~/constant/recommendKeyword';
7 | import { IdCardCreationFormModel } from '~/types/idCard';
8 |
9 | const title = '이웃 주민에게 자신을 소개할\n 키워드를 적어주세요!';
10 |
11 | export const KeywordStep = () => {
12 | const { control } = useFormContext();
13 |
14 | return (
15 |
16 |
{title}
17 | (
21 |
32 | )}
33 | />
34 |
35 | );
36 | };
37 |
--------------------------------------------------------------------------------
/src/modules/IdCardCreation/Step/LoadingStep.client.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import Image from 'next/image';
4 |
5 | type LoadingStepProps = {
6 | planetName: string;
7 | };
8 |
9 | export const LoadingStep = ({ planetName }: LoadingStepProps) => {
10 | return (
11 |
12 |
{`${planetName}으로\n광속으로 이동중...`}
13 |
22 |
23 | );
24 | };
25 |
--------------------------------------------------------------------------------
/src/modules/IdCardCreation/Step/index.ts:
--------------------------------------------------------------------------------
1 | export * from './BoardingStep.client';
2 | export * from './CompleteStep.client';
3 | export * from './KeywordContentImage.client';
4 | export * from './KeywordContentStep.client';
5 | export * from './KeywordStep.client';
6 | export * from './LoadingStep.client';
7 | export * from './ProfileStep.client';
8 |
--------------------------------------------------------------------------------
/src/modules/IdCardCreation/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Form';
2 | export * from './IdCardCreationSteps';
3 | export * from './Step';
4 |
--------------------------------------------------------------------------------
/src/modules/IdCardDetail/KeywordContentCard.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 |
3 | import { tw } from '~/utils/tailwind.util';
4 |
5 | type KeywordContentCardProps = {
6 | title: ReactNode | string;
7 | image: ReactNode | null;
8 | content: ReactNode | string;
9 | className?: string;
10 | onClick?: () => void;
11 | };
12 |
13 | export const KeywordContentCard = ({
14 | title,
15 | image,
16 | content,
17 | className,
18 | onClick,
19 | ...props
20 | }: KeywordContentCardProps) => {
21 | return (
22 |
23 |
{title}
24 |
25 | {image}
26 |
{content}
27 |
28 |
29 |
30 | );
31 | };
32 |
--------------------------------------------------------------------------------
/src/modules/IdCardDetail/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Intro';
2 | export * from './KeywordContentCard';
3 |
--------------------------------------------------------------------------------
/src/modules/IdCardEditButton/IdCardEditButton.client.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { usePathname, useRouter } from 'next/navigation';
3 |
4 | import { Button } from '~/components/Button';
5 |
6 | export const IdCardEditButton = () => {
7 | const pathname = usePathname();
8 | const router = useRouter();
9 |
10 | const onClickIdCardEditButton = () => {
11 | router.push(`${pathname}/edit`);
12 | };
13 |
14 | return (
15 |
23 | );
24 | };
25 |
--------------------------------------------------------------------------------
/src/modules/IdCardEditButton/IdCardEditButton.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { IdCardEditButton } from './index';
4 |
5 | const meta: Meta = {
6 | title: 'modules/IdCardEditButton',
7 | component: IdCardEditButton,
8 | args: {},
9 | };
10 |
11 | export default meta;
12 |
13 | type Story = StoryObj;
14 |
15 | export const Primary: Story = {
16 | render: () => ,
17 | };
18 |
--------------------------------------------------------------------------------
/src/modules/IdCardEditButton/index.ts:
--------------------------------------------------------------------------------
1 | export * from './IdCardEditButton.client';
2 |
--------------------------------------------------------------------------------
/src/modules/IdCardEditor/Form/IdCardEditorForm.tsx:
--------------------------------------------------------------------------------
1 | import { FormEvent } from 'react';
2 |
3 | import { EditorSteps } from '~/modules/IdCardEditor/IdCardEditor.type';
4 | import {
5 | EditKeywordContentStep,
6 | EditKeywordStep,
7 | EditProfileInfoStep,
8 | } from '~/modules/IdCardEditor/Step';
9 |
10 | type IdCardEditorFormProps = {
11 | steps: EditorSteps[];
12 | stepOrder: number;
13 |
14 | onClickMoveTargetStep: (targetStep: EditorSteps) => void;
15 | };
16 |
17 | export const IdCardEditorForm = ({
18 | steps,
19 | stepOrder,
20 | onClickMoveTargetStep,
21 | }: IdCardEditorFormProps) => {
22 | return (
23 |
30 | );
31 | };
32 |
--------------------------------------------------------------------------------
/src/modules/IdCardEditor/Form/index.ts:
--------------------------------------------------------------------------------
1 | export * from './IdCardEditorForm';
2 |
--------------------------------------------------------------------------------
/src/modules/IdCardEditor/IdCardEditor.constant.ts:
--------------------------------------------------------------------------------
1 | import { EditorSteps } from '~/modules/IdCardEditor/IdCardEditor.type';
2 |
3 | // 순서가 있지는 않음. KEYWORD_CONTENT: 최초 진인접, / PROFILE, KEYWORD은 같은 깊이
4 | export const editorSteps: EditorSteps[] = ['KEYWORD_CONTENT', 'PROFILE', 'KEYWORD'];
5 |
6 | export const KEYWORD_CONTENT_STEP = 0;
7 | export const PROFILE_STEP = 1;
8 | export const KEYWORD_STEP = 2;
9 |
10 | // TODO: 전체 form에서 사용되는 로직과 동일합니다! 추후 src/constant로 옮겨도 될 듯 해요~
11 | export const MAX_KEYWORD_LIST_LENGTH = 7;
12 | export const MAX_KEYWORD_INPUT_LENGTH = 8;
13 | export const MAX_NICKNAME_LENGTH = 16;
14 | export const MAX_ABOUT_ME_LENGTH = 50;
15 |
--------------------------------------------------------------------------------
/src/modules/IdCardEditor/IdCardEditor.type.ts:
--------------------------------------------------------------------------------
1 | import { IdCardEditorFormModel } from '~/types/idCard';
2 |
3 | export type EditorSteps = 'PROFILE' | 'KEYWORD' | 'KEYWORD_CONTENT';
4 |
5 | export type IdCardEditorFormValues = Omit;
6 |
--------------------------------------------------------------------------------
/src/modules/IdCardEditor/IdCardEditorSteps.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { IdCardEditor } from './index';
4 |
5 | const meta: Meta = {
6 | title: 'modules/IdCardEditor',
7 | component: IdCardEditor,
8 | args: {},
9 | };
10 |
11 | export default meta;
12 |
13 | type Story = StoryObj;
14 |
15 | export const Primary: Story = {
16 | render: () => ,
17 | };
18 |
--------------------------------------------------------------------------------
/src/modules/IdCardEditor/Step/index.ts:
--------------------------------------------------------------------------------
1 | export * from './EditKeywordContentStep.client';
2 | export * from './EditKeywordStep.client';
3 | export * from './EditProfileInfoStep.client';
4 |
--------------------------------------------------------------------------------
/src/modules/IdCardEditor/index.ts:
--------------------------------------------------------------------------------
1 | export * from './IdCardEditor';
2 |
--------------------------------------------------------------------------------
/src/modules/InvitationButtons/InvitationButtons.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { InvitationButtons } from './index';
4 |
5 | const meta: Meta = {
6 | title: 'modules/InvitationButtons',
7 | component: InvitationButtons,
8 | args: {},
9 | };
10 |
11 | export default meta;
12 |
13 | type Story = StoryObj;
14 |
15 | export const Primary: Story = {
16 | render: () => ,
17 | };
18 |
--------------------------------------------------------------------------------
/src/modules/InvitationButtons/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './InvitationButtons.client';
2 |
--------------------------------------------------------------------------------
/src/modules/KeywordContentEditCard/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './KeywordContentEditCard.client';
2 |
--------------------------------------------------------------------------------
/src/modules/LoginStep/AppleLoginButton.client.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import Image from 'next/image';
3 |
4 | export const AppleLoginButton = () => {
5 | return (
6 |
7 |
16 |
17 | );
18 | };
19 |
--------------------------------------------------------------------------------
/src/modules/LoginStep/LoginStep.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { LoginStep } from './index';
4 |
5 | const meta: Meta = {
6 | title: 'modules/LoginStep',
7 | component: LoginStep,
8 | args: {},
9 | };
10 |
11 | export default meta;
12 |
13 | type Story = StoryObj;
14 |
15 | export const Primary: Story = {
16 | render: () => ,
17 | };
18 |
--------------------------------------------------------------------------------
/src/modules/LoginStep/index.ts:
--------------------------------------------------------------------------------
1 | export * from './LoginStep.client';
2 |
--------------------------------------------------------------------------------
/src/modules/LoginStep/kakaoLoginButton.client.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import Image from 'next/image';
3 | import { ClientSafeProvider, signIn } from 'next-auth/react';
4 |
5 | type KakaoLoginButtonProps = {
6 | provider: ClientSafeProvider;
7 | };
8 |
9 | export const KakaoLoginButton = ({ provider }: KakaoLoginButtonProps) => {
10 | return (
11 |
12 |
28 |
29 | );
30 | };
31 |
--------------------------------------------------------------------------------
/src/modules/LoginStep/style.css:
--------------------------------------------------------------------------------
1 | .swiper-pagination-bullet {
2 | width: 8px !important;
3 | height: 8px !important;
4 | /* bg-grey-500 */
5 | background-color: #949494 !important;
6 | }
7 |
8 | .swiper-pagination-bullet-active {
9 | width: 28px !important;
10 | border-radius: 41px !important;
11 | /* bg-primary-500 */
12 | background-color: #5445ff !important;
13 | }
14 |
--------------------------------------------------------------------------------
/src/modules/Notification/Notification.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Meta } from '@storybook/react';
2 |
3 | import { createNotification, createNotificationList } from '~/mocks/notification/notification.mock';
4 |
5 | import { NotificationItem } from './NotificationItem';
6 | import { NotificationList } from './NotificationList';
7 | import { NotificationNoData } from './NotificationNoData';
8 |
9 | const meta: Meta = {
10 | title: 'modules/Notification',
11 | component: NotificationItem,
12 | args: {},
13 | };
14 |
15 | export default meta;
16 |
17 | const MOCK_NOTIFICATION = createNotification(123);
18 | export const Item = () => ;
19 |
20 | const MOCK_NOTIFICATIONS = createNotificationList(10, 1, 10);
21 | export const List = () => (
22 |
23 |
24 |
25 | );
26 |
27 | export const NoData = () => ;
28 |
--------------------------------------------------------------------------------
/src/modules/Notification/NotificationList.tsx:
--------------------------------------------------------------------------------
1 | import { NotificationModel } from '~/types/notification';
2 |
3 | import { NotificationItem } from './NotificationItem';
4 |
5 | type NotificationListProps = {
6 | notifications: NotificationModel[];
7 | notificationAgoList?: Record;
8 | };
9 | export const NotificationList = ({ notifications, notificationAgoList }: NotificationListProps) => {
10 | return (
11 |
24 | );
25 | };
26 |
--------------------------------------------------------------------------------
/src/modules/Notification/NotificationNoData.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image';
2 |
3 | export const NotificationNoData = () => {
4 | return (
5 |
6 |
11 |
12 |
13 | 아직 알람이 없어요.
14 |
15 | 다른 주민들에게 먼저 말을 건네보세요.
16 |
17 |
18 | );
19 | };
20 |
--------------------------------------------------------------------------------
/src/modules/Notification/NotificationTabItem.client.tsx:
--------------------------------------------------------------------------------
1 | import { CommunityLogoImage } from '~/modules/CommunityProfile';
2 | import { twMerge } from '~/utils/tailwind.util';
3 |
4 | import { CommunityNotification } from './NotificationTab.client';
5 |
6 | type NotificationTabProps = {
7 | community: CommunityNotification;
8 | isActive: boolean;
9 | onClick: (communityId: number) => void;
10 | };
11 | export const NotificationTabItem = ({ community, isActive, onClick }: NotificationTabProps) => {
12 | return (
13 | onClick(community.communityId)}
15 | className={twMerge(
16 | 'relative mr-24pxr flex items-center gap-4pxr border-b-2 border-b-white pb-10pxr pr-8pxr text-gray-400',
17 | isActive && 'border-b-black text-black',
18 | )}
19 | >
20 | {community.logoImageUrl && (
21 |
22 | )}
23 | {community.title}
24 | {community.hasNewNotification && (
25 |
28 | )}
29 |
30 | );
31 | };
32 |
--------------------------------------------------------------------------------
/src/modules/NudgeItem/NudgeItem.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { NudgeItem } from './NudgeItem';
4 |
5 | const meta: Meta = {
6 | title: 'modules/NudgeItem',
7 | component: NudgeItem,
8 | };
9 |
10 | type Story = StoryObj;
11 |
12 | const opponentUser = {
13 | nickname: '최예원',
14 | profileImageUrl:
15 | 'https://cloudflare-ipfs.com/ipfs/Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/1095.jpg',
16 | userId: 1,
17 | };
18 |
19 | export const Default: Story = {
20 | render: () => (
21 |
28 | ),
29 | };
30 |
31 | export default meta;
32 |
--------------------------------------------------------------------------------
/src/modules/NudgeItem/index.ts:
--------------------------------------------------------------------------------
1 | export * from './NudgeItem';
2 |
--------------------------------------------------------------------------------
/src/modules/NudgeList/NudgeList.client.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useGetNudgeList } from '~/api/domain/nudge.api.client';
4 | import { UseBottomSheetReturn } from '~/components/BottomSheet';
5 | import BottomSheet from '~/components/BottomSheet/BottomSheet';
6 | import { NudgeItem } from '~/modules/NudgeItem';
7 |
8 | type NudgeListProps = {
9 | bottomSheetHandlers: UseBottomSheetReturn;
10 | idCardsId: number;
11 | communityId: number;
12 | };
13 |
14 | export const NudgeList = ({ bottomSheetHandlers, idCardsId, communityId }: NudgeListProps) => {
15 | const { data } = useGetNudgeList(idCardsId);
16 | return (
17 |
18 |
19 | 받은 딩동
20 |
21 |
22 | {data?.nudgeInfoDtos.map((nudge, idx) => (
23 |
24 | ))}
25 |
26 |
27 |
28 |
29 | );
30 | };
31 |
--------------------------------------------------------------------------------
/src/modules/NudgeList/index.ts:
--------------------------------------------------------------------------------
1 | export * from './NudgeList.client';
2 |
--------------------------------------------------------------------------------
/src/modules/Onboarding/CharacterCreation.type.ts:
--------------------------------------------------------------------------------
1 | export type CharacterCreationFormType = {
2 | firstAlphabet: 'E' | 'I';
3 | secondAlphabet: 'F' | 'T';
4 | thirdAlphabet: 'J' | 'P';
5 | fourthAlphabet: 'S' | 'N';
6 | };
7 |
8 | export type CharacterAlphabetType = 'E' | 'I' | 'F' | 'T' | 'J' | 'P' | 'S' | 'N';
9 |
10 | export type CharacterCreationStepsType =
11 | | 'BOARDING'
12 | | 'FIRST'
13 | | 'SECOND'
14 | | 'THIRD'
15 | | 'FOURTH'
16 | | 'COMPLETE';
17 |
--------------------------------------------------------------------------------
/src/modules/Onboarding/CharacterCreationSteps.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { CharacterCreationSteps } from './CharacterCreationSteps.client';
4 |
5 | const meta: Meta = {
6 | title: 'modules/CharacterCreationSteps',
7 | component: CharacterCreationSteps,
8 | args: {},
9 | };
10 |
11 | export default meta;
12 |
13 | type Story = StoryObj;
14 |
15 | export const Default: Story = {};
16 |
--------------------------------------------------------------------------------
/src/modules/Onboarding/Form/index.ts:
--------------------------------------------------------------------------------
1 | export * from './CharacterCreationForm.client';
2 |
--------------------------------------------------------------------------------
/src/modules/Onboarding/Question/CharacterQuestion.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 | import { MouseEvent } from 'react';
3 |
4 | import { CharacterQuestion } from './CharacterQuestion';
5 |
6 | const meta: Meta = {
7 | title: 'modules/CharacterQuestion',
8 | component: CharacterQuestion,
9 | args: {},
10 | };
11 |
12 | export default meta;
13 |
14 | type Story = StoryObj;
15 |
16 | export const Default: Story = {
17 | args: {
18 | title: '홀로 우주 패키지 여행을\n 가게 되었다',
19 | image: '/assets/images/onboarding-question-ticket.png',
20 | firstOption: { fieldValue: 'E', content: '옆자리에 앉은 사람에게 말을 건다.' },
21 | secondOption: { fieldValue: 'I', content: '풍경을 보며 나만의 시간을 즐긴다.' },
22 | onQuestionButtonClick: (e: MouseEvent) => console.log(e.currentTarget.name),
23 | },
24 | };
25 |
--------------------------------------------------------------------------------
/src/modules/Onboarding/Question/CharacterQuestion.type.ts:
--------------------------------------------------------------------------------
1 | import { CharacterAlphabetType, CharacterCreationFormType } from '../CharacterCreation.type';
2 |
3 | type OptionType = {
4 | fieldValue: CharacterAlphabetType;
5 | content: string;
6 | };
7 |
8 | export type QuestionDetail = {
9 | title: string;
10 | image: string;
11 | fieldName: keyof CharacterCreationFormType;
12 | firstOption: OptionType;
13 | secondOption: OptionType;
14 | };
15 |
--------------------------------------------------------------------------------
/src/modules/Onboarding/Step/CharacterCompleteStep.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { CharacterCompleteStep } from './CharacterCompleteStep.client';
4 |
5 | const meta: Meta = {
6 | title: 'modules/CharacterCompleteStep',
7 | component: CharacterCompleteStep,
8 | args: {},
9 | };
10 |
11 | export default meta;
12 |
13 | type Story = StoryObj;
14 |
15 | export const Default: Story = {
16 | args: { characterName: 'BUDDY' },
17 | };
18 |
--------------------------------------------------------------------------------
/src/modules/Onboarding/Step/index.ts:
--------------------------------------------------------------------------------
1 | export * from './CharacterCompleteStep.client';
2 |
--------------------------------------------------------------------------------
/src/modules/Onboarding/index.ts:
--------------------------------------------------------------------------------
1 | export * from './CharacterCreation.type';
2 | export * from './CharacterCreationSteps.client';
3 | export * from './Form/CharacterCreationForm.client';
4 |
--------------------------------------------------------------------------------
/src/modules/PlanetCreationButton/PlanetCreationButton.client.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { useRouter } from 'next/navigation';
3 |
4 | import { PlusIcon } from '~/components/Icon';
5 | import { tw } from '~/utils/tailwind.util';
6 |
7 | export const PlanetCreationButton = () => {
8 | const router = useRouter();
9 |
10 | const onClickCreateButton = () => {
11 | router.push('/admin/planet/create');
12 | };
13 |
14 | return (
15 |
16 |
26 |
27 | );
28 | };
29 |
--------------------------------------------------------------------------------
/src/modules/PlanetCreationButton/PlanetCreationButton.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { PlanetCreationButton } from './index';
4 |
5 | const meta: Meta = {
6 | title: 'modules/PlanetCreationButton',
7 | component: PlanetCreationButton,
8 | args: {},
9 | };
10 |
11 | export default meta;
12 |
13 | type Story = StoryObj;
14 |
15 | export const Primary: Story = {
16 | render: () => ,
17 | };
18 |
--------------------------------------------------------------------------------
/src/modules/PlanetCreationButton/index.ts:
--------------------------------------------------------------------------------
1 | export * from './PlanetCreationButton.client';
2 |
--------------------------------------------------------------------------------
/src/modules/PlanetEnterButton/PlanetEnterButton.client.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useRouter } from 'next/navigation';
4 |
5 | import { Button, TextButton } from '~/components/Button';
6 | import { DINGDONG_PLANET } from '~/utils/variable';
7 |
8 | export const PlanetEnterButton = () => {
9 | const router = useRouter();
10 | return (
11 |
12 |
router.push(`/planet/${DINGDONG_PLANET.DINGDONG_PLANET_ID}`)}
15 | >
16 | 딩동행성 둘러보기
17 |
18 |
19 |
22 |
23 |
24 | );
25 | };
26 |
--------------------------------------------------------------------------------
/src/modules/PlanetEnterButton/PlanetEnterButton.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { PlanetEnterButton } from './PlanetEnterButton.client';
4 |
5 | const meta: Meta = {
6 | title: 'modules/PlanetEnterButton',
7 | component: PlanetEnterButton,
8 | };
9 |
10 | type Story = StoryObj;
11 |
12 | export const Default: Story = {
13 | render: () => (
14 |
17 | ),
18 | };
19 |
20 | export default meta;
21 |
--------------------------------------------------------------------------------
/src/modules/PlanetEnterButton/index.ts:
--------------------------------------------------------------------------------
1 | export * from './PlanetEnterButton.client';
2 |
--------------------------------------------------------------------------------
/src/modules/PlanetMenu/PlanetMenu.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { PlanetMenu } from './index';
4 |
5 | const meta: Meta = {
6 | title: 'modules/PlanetMenu',
7 | component: PlanetMenu,
8 | args: {},
9 | };
10 |
11 | export default meta;
12 |
13 | type Story = StoryObj;
14 |
15 | export const Primary: Story = {
16 | render: () => ,
17 | };
18 |
--------------------------------------------------------------------------------
/src/modules/PlanetMenu/index.ts:
--------------------------------------------------------------------------------
1 | export * from './PlanetMenu.client';
2 |
--------------------------------------------------------------------------------
/src/modules/PlanetSelector/PlanetSelector.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { TopNavigation } from '~/components/TopNavigation';
4 |
5 | import { PlanetSelector } from './index';
6 |
7 | const meta: Meta = {
8 | title: 'modules/PlanetSelector',
9 | component: PlanetSelector,
10 | args: {},
11 | };
12 |
13 | export default meta;
14 |
15 | type Story = StoryObj;
16 |
17 | export const Primary: Story = {
18 | render: () => (
19 |
20 |
21 |
22 |
23 |
24 | ),
25 | };
26 |
--------------------------------------------------------------------------------
/src/modules/PlanetSelector/index.ts:
--------------------------------------------------------------------------------
1 | export * from './PlanetSelector.client';
2 |
--------------------------------------------------------------------------------
/src/modules/UserMenu/UserMenu.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { UserMenu } from './index';
4 |
5 | const meta: Meta = {
6 | title: 'modules/UserMenu',
7 | component: UserMenu,
8 | args: {},
9 | };
10 |
11 | export default meta;
12 |
13 | type Story = StoryObj;
14 |
15 | export const Primary: Story = {
16 | render: () => ,
17 | };
18 |
--------------------------------------------------------------------------------
/src/modules/UserMenu/index.ts:
--------------------------------------------------------------------------------
1 | export * from './UserMenu.client';
2 |
--------------------------------------------------------------------------------
/src/stores/comment.store.ts:
--------------------------------------------------------------------------------
1 | import { create } from 'zustand';
2 |
3 | type ReplyRecipient = {
4 | nickname: string | undefined;
5 | commentId: number | undefined;
6 | setReplyRecipient: (nickname: string, commentId: number) => void;
7 | clear: () => void;
8 | };
9 |
10 | export const useReplyRecipientStore = create()(set => ({
11 | nickname: undefined,
12 | commentId: undefined,
13 | setReplyRecipient: (nickname: string, commentId: number) => {
14 | set(() => ({
15 | nickname: nickname,
16 | commentId: commentId,
17 | }));
18 | },
19 | clear: () => {
20 | set(() => ({
21 | nickname: undefined,
22 | commentId: undefined,
23 | }));
24 | },
25 | }));
26 |
--------------------------------------------------------------------------------
/src/stores/community.store.ts:
--------------------------------------------------------------------------------
1 | import { create } from 'zustand';
2 |
3 | import { DINGDONG_PLANET } from '~/utils/variable';
4 |
5 | type Store = {
6 | communityId: number;
7 | switchCommunity: (id: number) => void;
8 | isInitPlanetId: () => boolean;
9 | };
10 |
11 | export const useCommunityStore = create()((set, get) => ({
12 | communityId: DINGDONG_PLANET.DINGDONG_PLANET_ID,
13 | switchCommunity: (id: number) => {
14 | document.cookie = `communityId=${id}`;
15 | set(() => ({
16 | communityId: id,
17 | }));
18 | },
19 | isInitPlanetId: () => {
20 | return get().communityId === DINGDONG_PLANET.DINGDONG_PLANET_ID;
21 | },
22 | }));
23 |
--------------------------------------------------------------------------------
/src/types/api/index.ts:
--------------------------------------------------------------------------------
1 | export * from './response.type';
2 |
--------------------------------------------------------------------------------
/src/types/api/response.type.ts:
--------------------------------------------------------------------------------
1 | export type SliceResponse = {
2 | content: T[];
3 | page: number;
4 | size: number;
5 | hasNext: boolean;
6 | };
7 |
--------------------------------------------------------------------------------
/src/types/auth/index.ts:
--------------------------------------------------------------------------------
1 | export * from './response.type';
2 |
--------------------------------------------------------------------------------
/src/types/auth/model.type.ts:
--------------------------------------------------------------------------------
1 | export type KakaoPropertiesModel = {
2 | nickname: string;
3 | profile_image: string;
4 | thumbnail_image: string;
5 | };
6 |
7 | export type KakaoAccountModel = {
8 | email: string;
9 | age_range: string;
10 | birthday: string;
11 | gender: string;
12 | };
13 |
--------------------------------------------------------------------------------
/src/types/auth/response.type.ts:
--------------------------------------------------------------------------------
1 | import { KakaoAccountModel, KakaoPropertiesModel } from '~/types/auth/model.type';
2 |
3 | export type AuthResponse = {
4 | data: unknown;
5 | accessToken: string;
6 | refreshToken: string;
7 | userId: number;
8 | accessTokenExpireDate: number;
9 | success?: boolean;
10 | };
11 |
12 | export const AUTH_COOKIE_KEYS: Record = {
13 | accessToken: 'dingdong_at',
14 | refreshToken: 'dingdong_rt',
15 | userId: 'dingdong_uid',
16 | accessTokenExpireDate: 'dingdong_at_expire_date',
17 | } as const;
18 |
19 | export type KakaoUserInfoResponse = {
20 | id: string;
21 | properties: KakaoPropertiesModel;
22 | kakao_account: KakaoAccountModel;
23 | };
24 |
--------------------------------------------------------------------------------
/src/types/comment/index.ts:
--------------------------------------------------------------------------------
1 | export * from './model.type';
2 | export * from './request.type';
3 | export * from './response.type';
4 |
--------------------------------------------------------------------------------
/src/types/comment/model.type.ts:
--------------------------------------------------------------------------------
1 | export type CommentLikeModel = {
2 | likeCount: number;
3 | likedByCurrentUser: boolean;
4 | };
5 |
6 | export type CommentWriterIntoModel = {
7 | userId: number;
8 | nickname: string;
9 | profileImageUrl: string;
10 | };
11 |
12 | export type CommentModel = {
13 | idCardId: number;
14 | commentId: number;
15 | content: string;
16 | createdAt: string;
17 | writerInfo: CommentWriterIntoModel;
18 | commentLikeInfo: CommentLikeModel;
19 | repliesCount: number;
20 | };
21 |
22 | export type CommentReplyModel = {
23 | commentReplyId: number;
24 | content: string;
25 | createdAt: string;
26 | writerInfo: CommentWriterIntoModel;
27 | commentReplyLikeInfo: CommentLikeModel;
28 | };
29 |
--------------------------------------------------------------------------------
/src/types/comment/response.type.ts:
--------------------------------------------------------------------------------
1 | import { SliceResponse } from '~/types/api';
2 | import { CommentModel, CommentReplyModel } from '~/types/comment/model.type';
3 |
4 | export type CommentGetResponse = SliceResponse;
5 |
6 | export type CommentCountGetResponse = {
7 | count: number;
8 | };
9 |
10 | export type CommentReplyGetResponse = {
11 | commentId: number;
12 | repliesInfo: CommentReplyModel[];
13 | };
14 |
15 | export type CommentPostResponse = {
16 | id: number;
17 | };
18 |
19 | export type CommentDeleteResponse = {
20 | id: number;
21 | };
22 |
23 | export type CommentPostReplyResponse = {
24 | id: number;
25 | };
26 |
27 | export type CommentDeleteReplyResponse = {
28 | id: number;
29 | };
30 |
31 | export type CommentLikePostResponse = {
32 | id: number;
33 | };
34 |
35 | export type CommentReplyLikePostResponse = {
36 | id: number;
37 | };
38 |
39 | export type CommentLikeCancelDeleteResponse = {
40 | id: number;
41 | };
42 |
43 | export type CommentReplyLikeCancelDeleteResponse = {
44 | id: number;
45 | };
46 |
--------------------------------------------------------------------------------
/src/types/community/index.ts:
--------------------------------------------------------------------------------
1 | export * from './model.type';
2 | export * from './request.type';
3 | export * from './response.type';
4 |
--------------------------------------------------------------------------------
/src/types/community/request.type.ts:
--------------------------------------------------------------------------------
1 | import { CommunityJoinModel } from './model.type';
2 |
3 | export type CommunityIdCardsRequest = {
4 | pageParam?: number;
5 | communityId: number;
6 | };
7 |
8 | export type CreateCommunityRequest = {
9 | name: string;
10 | logoImageUrl?: string;
11 | coverImageUrl?: string;
12 | description?: string;
13 | };
14 |
15 | export type CommunityJoinRequest = CommunityJoinModel;
16 |
--------------------------------------------------------------------------------
/src/types/community/response.type.ts:
--------------------------------------------------------------------------------
1 | import { SliceResponse } from '~/types/api';
2 | import {
3 | CheckIdCardModel,
4 | CommunityDetailModel,
5 | CommunityIdCardsModel,
6 | CommunityListModel,
7 | CommunityUserInfoModel,
8 | InvitationCodeValidationModel,
9 | } from '~/types/community';
10 |
11 | export type CommunityIdCardsResponse = SliceResponse;
12 |
13 | export type CommunityDetailResponse = {
14 | communityDetailsDto: CommunityDetailModel;
15 | };
16 |
17 | export type CommunityListResponse = {
18 | communityListDtos: CommunityListModel[];
19 | };
20 |
21 | export type CommunityUpdateResponse = {
22 | id: number;
23 | };
24 |
25 | export type InvitationCodeValidationResponse = InvitationCodeValidationModel;
26 | export type CommunityNameCheckResponse = {
27 | data: boolean;
28 | };
29 |
30 | export type CheckIdCardResponse = CheckIdCardModel;
31 |
32 | export type CommunityUserInfoResponse = {
33 | myInfoInInCommunityDto: CommunityUserInfoModel;
34 | };
35 |
--------------------------------------------------------------------------------
/src/types/errorCodes.ts:
--------------------------------------------------------------------------------
1 | export const AUTH_ERROR_CODES = {
2 | UNAUTHORIZED_ERROR: '401',
3 | EXPIRED_TOKEN_ERROR: '401-1',
4 | INVALID_TOKEN_ERROR: '401-2',
5 | EXPIRED_REFRESH_TOKEN_ERROR: '401-3',
6 | NOT_VALID_ACCESS_TOKEN_ERROR: '401-4',
7 | } as const;
8 |
--------------------------------------------------------------------------------
/src/types/idCard/index.ts:
--------------------------------------------------------------------------------
1 | export * from './model.type';
2 | export * from './request.type';
3 | export * from './response.type';
4 |
--------------------------------------------------------------------------------
/src/types/idCard/request.type.ts:
--------------------------------------------------------------------------------
1 | import { IdCardCreationFormModel, IdCardEditorFormModel } from '~/types/idCard';
2 |
3 | export type CreateIdCardRequest = IdCardCreationFormModel;
4 | export type IdCardCreateRequest = IdCardCreationFormModel;
5 |
6 | export type EditIdCardRequest = IdCardEditorFormModel;
7 |
--------------------------------------------------------------------------------
/src/types/idCard/response.type.ts:
--------------------------------------------------------------------------------
1 | import { IdCardDetailModel } from '~/types/idCard';
2 |
3 | export type CommentCountResponse = {
4 | count: number;
5 | };
6 |
7 | export type IdCardDetailResponse = {
8 | idCardDetailsDto: IdCardDetailModel;
9 | };
10 |
11 | export type CommunityMyIdCardDetailResponse = {
12 | idCardDetailsDto: IdCardDetailModel;
13 | };
14 |
15 | export type IdCardCreateResponse = { id: number };
16 |
17 | export type IdCardEditResponse = {
18 | id: number;
19 | };
20 |
--------------------------------------------------------------------------------
/src/types/image/index.ts:
--------------------------------------------------------------------------------
1 | export * from './model.type';
2 |
--------------------------------------------------------------------------------
/src/types/image/model.type.ts:
--------------------------------------------------------------------------------
1 | export type ImageUrlModel = {
2 | imageUrl: string;
3 | };
4 |
5 | export type ImageFileModel = File;
6 |
--------------------------------------------------------------------------------
/src/types/image/request.type.ts:
--------------------------------------------------------------------------------
1 | import { ImageFileModel } from './model.type';
2 |
3 | export type ImageFileRequest = ImageFileModel;
4 |
--------------------------------------------------------------------------------
/src/types/image/response.type.ts:
--------------------------------------------------------------------------------
1 | import { ImageUrlModel } from './model.type';
2 |
3 | export type IamgeUrlResponse = ImageUrlModel;
4 |
--------------------------------------------------------------------------------
/src/types/notification/index.ts:
--------------------------------------------------------------------------------
1 | export * from './model.type';
2 | export * from './request.type';
3 | export * from './response.type';
4 |
--------------------------------------------------------------------------------
/src/types/notification/request.type.ts:
--------------------------------------------------------------------------------
1 | export type NotificationGetRequest = {
2 | pageParam: number;
3 | };
4 |
--------------------------------------------------------------------------------
/src/types/notification/response.type.ts:
--------------------------------------------------------------------------------
1 | import { NotificationModel } from '~/types/notification';
2 |
3 | export type NotificationGetResponse = {
4 | notificationDtos: NotificationModel[];
5 | };
6 |
7 | export type UnreadNotification = {
8 | data: boolean;
9 | };
10 |
--------------------------------------------------------------------------------
/src/types/nudge/index.ts:
--------------------------------------------------------------------------------
1 | export * from './model.type';
2 |
--------------------------------------------------------------------------------
/src/types/nudge/model.type.ts:
--------------------------------------------------------------------------------
1 | export type NudgeModel = 'FRIENDLY' | 'SIMILARITY' | 'TALKING' | 'MEET';
2 |
3 | export type NudgeListModel = {
4 | nudgeId: number;
5 | opponentUser: {
6 | userId: number;
7 | profileImageUrl: string;
8 | nickname: string;
9 | };
10 | toUserNudgeType: NudgeModel;
11 | fromUserNudgeType: NudgeModel | null;
12 | };
13 |
14 | export type NudgeIconSelectorType = 'DEFAULT' | NudgeModel;
15 |
16 | export type NudgeMessagesType = { text: string; id: NudgeModel }[];
17 |
18 | export const nudgeMessages: NudgeMessagesType = [
19 | {
20 | id: 'MEET',
21 | text: '만나서 반가워요',
22 | },
23 | {
24 | id: 'FRIENDLY',
25 | text: '친해지고 싶어요',
26 | },
27 | {
28 | id: 'SIMILARITY',
29 | text: '저와 비슷해요',
30 | },
31 | {
32 | id: 'TALKING',
33 | text: '같이 밥 한끼 해요',
34 | },
35 | ];
36 |
--------------------------------------------------------------------------------
/src/types/nudge/request.type.ts:
--------------------------------------------------------------------------------
1 | import { NudgeModel } from '~/types/nudge/model.type';
2 |
3 | export type NudgePostRequest = { nudgeType: NudgeModel; communityId: number };
4 |
5 | export type NudgePutRequest = NudgePostRequest;
6 |
--------------------------------------------------------------------------------
/src/types/nudge/response.type.ts:
--------------------------------------------------------------------------------
1 | import { NudgeListModel } from '~/types/nudge/model.type';
2 |
3 | export type NudgeListResponse = {
4 | nudgeInfoDtos: NudgeListModel[];
5 | };
6 |
--------------------------------------------------------------------------------
/src/types/user/index.ts:
--------------------------------------------------------------------------------
1 | export * from './model.type';
2 | export * from './request.type';
3 | export * from './response.type';
4 |
--------------------------------------------------------------------------------
/src/types/user/model.type.ts:
--------------------------------------------------------------------------------
1 | import { CharacterNameModel } from '../idCard';
2 |
3 | export type UserInfoModel = {
4 | userId: number;
5 | email: string;
6 | nickname: string;
7 | gender: string;
8 | ageRange: string;
9 | profileImageUrl: string;
10 | characterType?: CharacterNameModel;
11 | communityIds: number[];
12 | };
13 |
14 | export type CharacterCreateModel = CharacterNameModel;
15 |
--------------------------------------------------------------------------------
/src/types/user/request.type.ts:
--------------------------------------------------------------------------------
1 | // user.d.ts 정리 후 절대경로 적용
2 | import { CharacterCreateModel } from './model.type';
3 | import { UserInfoResponse } from './response.type';
4 |
5 | export type UserInfoRequest = Omit;
6 |
7 | export type CharacterCreateRequest = CharacterCreateModel;
8 |
--------------------------------------------------------------------------------
/src/types/user/response.type.ts:
--------------------------------------------------------------------------------
1 | import { UserInfoModel } from './model.type';
2 |
3 | export type UserInfoResponse = {
4 | userProfileDto: UserInfoModel;
5 | };
6 |
--------------------------------------------------------------------------------
/src/types/util.d.ts:
--------------------------------------------------------------------------------
1 | export type ClassNameType = ClassNameArray | string | null | undefined | 0 | false;
2 |
--------------------------------------------------------------------------------
/src/utils/auth/error.ts:
--------------------------------------------------------------------------------
1 | import { ApiError } from '~/api/config/customError';
2 |
3 | export class UserIdNotFoundError extends Error {
4 | constructor() {
5 | super('로그인이 필요합니다.');
6 | this.name = 'UserIdNotFoundError';
7 | }
8 | }
9 |
10 | export const isUnauthorizedError = (error: unknown): boolean => {
11 | if (error instanceof ApiError) {
12 | if (error.statusCode === 401) {
13 | return true;
14 | }
15 | }
16 | // NOTE: redirect가 server side(컴포넌트 외부)에서는 NEXT_REDIRECT 에러를 던지는 것으로 동작합니다. https://github.com/vercel/next.js/issues/42556
17 | // interceptor.server.ts의 onResponseErrorServer 함수에서 미로그인시 NEXT_REDIRECT 에러를 던지고 있습니다.
18 | if (
19 | error &&
20 | typeof error === 'object' &&
21 | 'message' in error &&
22 | error['message'] === 'NEXT_REDIRECT'
23 | ) {
24 | return true;
25 | }
26 | return false;
27 | };
28 |
--------------------------------------------------------------------------------
/src/utils/auth/getUserId.client.ts:
--------------------------------------------------------------------------------
1 | import { UserIdNotFoundError } from './error';
2 | import { getAuthTokensByCookie } from './tokenHandlers';
3 |
4 | export const getUserIdClient = (): number | undefined => {
5 | try {
6 | if (typeof document === 'undefined') throw new UserIdNotFoundError();
7 | const { userId } = getAuthTokensByCookie(document.cookie);
8 | if (userId !== undefined) return userId;
9 | throw new UserIdNotFoundError(); // 비로그인한 사용자의 경우
10 | } catch (e) {
11 | return undefined;
12 | }
13 | };
14 |
--------------------------------------------------------------------------------
/src/utils/auth/getUserId.server.ts:
--------------------------------------------------------------------------------
1 | import { cookies } from 'next/headers';
2 |
3 | import { AUTH_COOKIE_KEYS } from '~/types/auth';
4 |
5 | import { UserIdNotFoundError } from './error';
6 |
7 | export const getUserIdServer = (): number | undefined => {
8 | try {
9 | const cookieStore = cookies();
10 | const userId = cookieStore.get(AUTH_COOKIE_KEYS.userId)?.value;
11 | if (userId === undefined) throw new UserIdNotFoundError();
12 | const userIdNumber = Number(userId);
13 | if (isNaN(userIdNumber)) throw new UserIdNotFoundError();
14 | return userIdNumber;
15 | } catch (e) {
16 | return undefined;
17 | }
18 | };
19 |
--------------------------------------------------------------------------------
/src/utils/auth/loginProviders.ts:
--------------------------------------------------------------------------------
1 | import { ClientSafeProvider } from 'next-auth/react';
2 |
3 | export const KAKAO_PROVIDER: ClientSafeProvider = {
4 | callbackUrl: '/auth/callback/kakao',
5 | id: 'kakao',
6 | name: 'Kakao',
7 | type: 'oauth',
8 | signinUrl: '/auth/signin/kakao',
9 | };
10 |
--------------------------------------------------------------------------------
/src/utils/auth/tokenValidator.client.ts:
--------------------------------------------------------------------------------
1 | import { ApiError } from '~/api/config/customError';
2 | import { reissue } from '~/api/domain/auth.api.client';
3 | import { AuthResponse } from '~/types/auth';
4 |
5 | import { generateCookiesKeyValues } from './tokenHandlers';
6 |
7 | export const getAccessTokenClient = async (
8 | authTokens: Partial,
9 | ): Promise => {
10 | try {
11 | const { refreshToken } = authTokens;
12 | if (refreshToken) {
13 | // token refresh 로직 처리
14 | const { success, ...tokens } = await reissue(refreshToken);
15 | if (!success) {
16 | return null;
17 | }
18 | for (const [cookieKey, cookieValue] of generateCookiesKeyValues(tokens)) {
19 | document.cookie = `${cookieKey}=${cookieValue}; path=/;`;
20 | }
21 |
22 | return tokens.accessToken;
23 | } else {
24 | return null;
25 | }
26 | } catch (e) {
27 | return e as ApiError;
28 | }
29 | };
30 |
--------------------------------------------------------------------------------
/src/utils/auth/tokenValidator.server.ts:
--------------------------------------------------------------------------------
1 | import { cookies } from 'next/headers';
2 |
3 | import { ApiError } from '~/api/config/customError';
4 | import { reissue } from '~/api/domain/auth.api.server';
5 | import { AuthResponse } from '~/types/auth';
6 |
7 | import { generateCookiesKeyValues } from './tokenHandlers';
8 |
9 | export const getAccessTokenServer = async (
10 | authTokens: Partial,
11 | ): Promise => {
12 | try {
13 | const { refreshToken } = authTokens;
14 | if (refreshToken) {
15 | // token refresh 로직 처리
16 | const { success, ...tokens } = await reissue(refreshToken);
17 | if (!success) {
18 | return null;
19 | }
20 | const cookieStore = cookies();
21 | for (const [cookieKey, cookieValue] of generateCookiesKeyValues(tokens)) {
22 | cookieStore.set(cookieKey, cookieValue as string);
23 | }
24 |
25 | return tokens.accessToken;
26 | } else {
27 | return null;
28 | }
29 | } catch (e) {
30 | return e as ApiError;
31 | }
32 | };
33 |
--------------------------------------------------------------------------------
/src/utils/cookie.util.ts:
--------------------------------------------------------------------------------
1 | // Ref : https://ko.javascript.info/cookie
2 |
3 | export const getCookie = (name: string) => {
4 | const matches =
5 | typeof window !== 'undefined' &&
6 | document.cookie.match(
7 | // eslint-disable-next-line no-useless-escape
8 | new RegExp('(?:^|; )' + name.replace(/([\.$?*|{}\(\)\[\]\\\/\+^])/g, '\\$1') + '=([^;]*)'),
9 | );
10 | return matches ? decodeURIComponent(matches[1]) : undefined;
11 | };
12 |
13 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
14 | export const setCookie = (name: string, value: any, options?: any) => {
15 | const cookieOptions = {
16 | path: '/',
17 | ...options,
18 | };
19 |
20 | let updatedCookie = encodeURIComponent(name) + '=' + encodeURIComponent(value);
21 |
22 | for (const optionKey in cookieOptions) {
23 | updatedCookie += '; ' + optionKey;
24 | const optionValue = cookieOptions[optionKey];
25 | if (optionValue !== true) {
26 | updatedCookie += '=' + optionValue;
27 | }
28 | }
29 | document.cookie = updatedCookie;
30 | };
31 |
32 | export const deleteCookie = (name: string) => {
33 | setCookie(name, '', {
34 | 'max-age': -1,
35 | });
36 | };
37 |
--------------------------------------------------------------------------------
/src/utils/route/route.ts:
--------------------------------------------------------------------------------
1 | export const ROUTE_COOKIE_KEYS = {
2 | redirectUri: 'redirectUri',
3 | };
4 |
--------------------------------------------------------------------------------
/src/utils/util.common.ts:
--------------------------------------------------------------------------------
1 | export const isProd = (env: string): boolean => env === 'production';
2 |
3 | type Entries = {
4 | [K in keyof T]: [K, T[K]];
5 | }[keyof T][];
6 |
7 | export const getEntries = (obj: T) => Object.entries(obj) as Entries;
8 |
9 | export const isEqual = (obj1: any, obj2: any): boolean => {
10 | // 값이 같으면 true 반환
11 | if (obj1 === obj2) {
12 | return true;
13 | }
14 |
15 | // 객체 혹은 배열인 경우
16 | if (typeof obj1 === 'object' && typeof obj2 === 'object' && obj1 !== null && obj2 !== null) {
17 | const keys1 = Object.keys(obj1);
18 | const keys2 = Object.keys(obj2);
19 |
20 | // 속성 개수가 다른 경우 false 반환
21 | if (keys1.length !== keys2.length) {
22 | return false;
23 | }
24 |
25 | // 모든 속성에 대해 재귀적으로 비교
26 | for (const key of keys1) {
27 | if (!isEqual(obj1[key], obj2[key])) {
28 | return false;
29 | }
30 | }
31 |
32 | return true;
33 | }
34 |
35 | // 나머지 경우는 값이 다르므로 false 반환
36 | return false;
37 | };
38 |
39 | export const isEmptyText = (text: string) => {
40 | if (!text || text.trim() === '') {
41 | return true;
42 | } else {
43 | return false;
44 | }
45 | };
46 |
--------------------------------------------------------------------------------
/src/utils/validate.ts:
--------------------------------------------------------------------------------
1 | export const isValidUrl = (url: string) => {
2 | try {
3 | new URL(url);
4 | return true;
5 | } catch (e) {
6 | return false;
7 | }
8 | };
9 |
--------------------------------------------------------------------------------
/src/utils/variable.ts:
--------------------------------------------------------------------------------
1 | export const DINGDONG_PLANET = {
2 | DINGDONG_PLANET_ID: 1,
3 | CHARACTER_PIPI: 4,
4 | CHARACTER_TRUE: 3,
5 | CHARACTER_TOBBY: 2,
6 | CHARACTER_BUDDY: 1,
7 | };
8 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "target": "es5",
5 | "lib": ["dom", "dom.iterable", "esnext"],
6 | "allowJs": true,
7 | "skipLibCheck": true,
8 | "strict": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "noEmit": true,
11 | "esModuleInterop": true,
12 | "module": "esnext",
13 | "moduleResolution": "node",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "jsx": "preserve",
17 | "incremental": true,
18 | "plugins": [
19 | {
20 | "name": "next"
21 | }
22 | ],
23 | "paths": {
24 | "~/*": ["./src/*"]
25 | }
26 | },
27 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
28 | "exclude": ["node_modules"]
29 | }
30 |
--------------------------------------------------------------------------------