Skip to content

Commit

Permalink
feat(memo): 메모 API 구현 (#25)
Browse files Browse the repository at this point in the history
* memo

* fix

* feat

* fix

* fix

* f

* feat

* feat: post

* put

* fix

* f
  • Loading branch information
Collection50 authored Aug 19, 2024
1 parent e69a4a7 commit 844a7b2
Show file tree
Hide file tree
Showing 13 changed files with 397 additions and 2 deletions.
Binary file not shown.
13 changes: 13 additions & 0 deletions src/app/(sidebar)/write/[id]/api/useDeleteMemo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { http } from '@/apis/http';
import { useMutation } from '@tanstack/react-query';

export const deleteMemo = (cardId: string, memoId: number) =>
http.delete({
url: `/cards/${cardId}/card-memo/${memoId}`,
});

export const useDeleteMemo = (cardId: string) =>
useMutation({
mutationKey: ['delete-memo', cardId],
mutationFn: (memoId: number) => deleteMemo(cardId, memoId),
});
20 changes: 20 additions & 0 deletions src/app/(sidebar)/write/[id]/api/useGetMemos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { http } from '@/apis/http';
import { useSuspenseQuery } from '@tanstack/react-query';

export type GetMemosResponse = Array<{
id: number;
content: string;
updatedAt: string;
}>;

const getMemos = (cardId: string) =>
http.get<GetMemosResponse>({
url: `/cards/${cardId}/card-memo`,
});

export const useGetMemos = (cardId: string) =>
useSuspenseQuery({
queryKey: ['get-memos', cardId],
queryFn: () => getMemos(cardId),
select: ({ result }) => result,
});
14 changes: 14 additions & 0 deletions src/app/(sidebar)/write/[id]/api/usePostMemo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { http } from '@/apis/http';
import { useMutation } from '@tanstack/react-query';

export const postMemo = (cardId: string, content: string) =>
http.post({
url: `/cards/${cardId}/card-memo`,
data: { content },
});

export const usePostMemo = (cardId: string) =>
useMutation({
mutationKey: ['post-memo', cardId],
mutationFn: (content: string) => postMemo(cardId, content),
});
14 changes: 14 additions & 0 deletions src/app/(sidebar)/write/[id]/api/usesPutMemo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { http } from '@/apis/http';
import { useMutation } from '@tanstack/react-query';

export const putMemo = (cardId: string, memoId: number, content: string) =>
http.put({
url: `/cards/${cardId}/card-memo/${memoId}/content`,
data: { content },
});

export const usePutMemo = (cardId: string) =>
useMutation({
mutationKey: ['put-memo', cardId],
mutationFn: ({ memoId, content }: { memoId: number; content: string }) => putMemo(cardId, memoId, content),
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
'use client';

import { useState, useRef, useEffect, useCallback } from 'react';
import { Textarea } from '@/system/components/Textarea/Textarea';
import { RemoveMemo } from '@/system/components/Icon/SVG/RemoveMemo';
import { AnimatePresence } from 'framer-motion';
import { motion } from 'framer-motion';
import { GetMemosResponse } from '../../../api/useGetMemos';
import { useMemosContext } from '../../../fetcher/MemosFetcher';
import { useDeleteMemo } from '../../../api/useDeleteMemo';
import { usePutMemo } from '@/app/(sidebar)/write/[id]/api/usesPutMemo';

export default function Memo({ id: memoId, content, updatedAt }: GetMemosResponse[number]) {
const { cardId } = useMemosContext();
const prevMemo = useRef<string>(content);
const [memo, setMemo] = useState(content || '');
const [showCloseButton, setShowCloseButton] = useState(false);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const { mutate } = useDeleteMemo(cardId);
const { mutate: putMemo } = usePutMemo(cardId);

const adjustTextareaHeight = useCallback(() => {
const textarea = textareaRef.current;

if (textarea) {
textarea.style.height = 'auto';
textarea.style.height = `${textarea.scrollHeight}px`;
}
}, []);

useEffect(() => {
adjustTextareaHeight();

if (prevMemo.current !== memo) {
putMemo({ memoId, content: memo });
}
}, [memo]);

return (
<div
// using pure css in memo.css
className="memo-wrap"
onMouseEnter={() => setShowCloseButton(true)}
onMouseLeave={() => setShowCloseButton(false)}>
<div className="absolute bottom-27 w-360 z-[100] h-2 bg-white" />

<div className="w-360 h-auto bg-white rounded-tl-8 rounded-tr-8 px-16 pt-16 pb-8">
<Textarea
ref={textareaRef}
rows={1}
className="w-full min-h-0 border-none p-0 memo-14 resize-none focus:outline-0 focus-visible:ring-0 focus-visible:ring-offset-0 rounded-0"
value={memo}
onChange={(e) => setMemo(e.target.value)}
autoFocus
/>
</div>

<AnimatePresence mode="wait">
{showCloseButton && (
<motion.button
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.2 }}
exit={{ opacity: 0 }}
className="absolute top-16 right-32"
onClick={() => mutate(memoId)}>
<RemoveMemo size={24} color="#37383C" />
</motion.button>
)}
</AnimatePresence>

<div className="memo pl-16 memo-10 pb-16 memo-neutral-35">{updatedAt.split(' ')[0].replaceAll('-', '.')}</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Button, Icon } from '@/system/components';
import { Textarea } from '@/system/components/Textarea/Textarea';
import { useState } from 'react';
import Memo from './Memo/Memo';
import { cn } from '@/utils';
import { useMemosContext } from '../../fetcher/MemosFetcher';
import { usePostMemo } from '@/app/(sidebar)/write/[id]/api/usePostMemo';

const TEXT_DEFAULT_HEIGHT = 22;
const TEXT_FOCUS_HEIGHT = 80;

export default function MemoContainer() {
const { memos, cardId } = useMemosContext();
const [memo, setMemo] = useState<string>('');
const [textareaHeight, setTextareaHeight] = useState(TEXT_DEFAULT_HEIGHT);
const { mutate } = usePostMemo(cardId);

return (
<section className="min-w-400 h-screen border-1 bg-neutral-1">
<div className="flex items-end p-16 w-full h-109 gap-8">
<Icon name="filledMemo" size={24} />
<p className="text-18 font-semibold">메모</p>
</div>

<div className="w-full h-[calc(100vh-294px)] px-16 flex flex-col gap-16 overflow-y-scroll">
{memos.map((memo) => (
<Memo key={memo.id} {...memo} />
))}
</div>

<div className="max-w-400 relative px-16 pt-16 pb-24 h-185 flex flex-col justify-end">
<div className="pt-13 px-16 pb-8 rounded-8 border-1 border-neutral-20 bg-white flex flex-col gap-4">
<Textarea
value={memo}
onChange={(e) => setMemo(e.target.value)}
onFocus={() => setTextareaHeight(TEXT_FOCUS_HEIGHT)}
onBlur={() => setTextareaHeight(TEXT_DEFAULT_HEIGHT)}
rows={1}
className={cn(
'resize-none min-h-0 bg-white border-none focus:outline-0 focus-visible:ring-0 focus-visible:ring-offset-0',
textareaHeight === TEXT_DEFAULT_HEIGHT && 'overflow-hidden',
)}
style={{ height: `${textareaHeight}px`, transition: 'height 0.2s ease-in-out' }}
maxLength={130}
/>

<div className="flex justify-between items-center w-full h-32">
<p className="text-10 text-neutral-60">{memo.length} / 130</p>
<Button onClick={() => mutate(memo)}>
<Icon name="submitArrow" size={32} color="#1B1C1E" />
</Button>
</div>
</div>
</div>
</section>
);
}
19 changes: 19 additions & 0 deletions src/app/(sidebar)/write/[id]/fetcher/MemosFetcher.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { generateContext } from '@/lib';
import { GetMemosResponse, useGetMemos } from '../api/useGetMemos';
import { StrictPropsWithChildren } from '@/types';

const [MemosProvider, useMemosContext] = generateContext<{ memos: GetMemosResponse; cardId: string }>({
name: 'memos-provider',
});

function MemosFetcher({ cardId, children }: StrictPropsWithChildren<{ cardId: string }>) {
const { data } = useGetMemos(cardId);

return (
<MemosProvider memos={data} cardId={cardId}>
{children}
</MemosProvider>
);
}

export { MemosFetcher, useMemosContext };
Loading

0 comments on commit 844a7b2

Please sign in to comment.