Skip to content

Commit 1c027bc

Browse files
committed
feat: 토픽, 커스텀 스티커 상세 조회 API 연동
1 parent 42e3077 commit 1c027bc

16 files changed

Lines changed: 275 additions & 36 deletions

File tree

apps/web/src/components/(with-side-bar)/topic/add-sticker-dialog.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ function AddStickerDialogContent({ topicId }: AddStickerDialogContentProps) {
4040

4141
await createCustomSticker(body, {
4242
onSuccess: () => {
43-
invalidateQueries([TOPIC.GET_TOPIC_BY_ID, topicId]);
43+
invalidateQueries([TOPIC.GET_TOPIC_BOARD_BY_ID, topicId]);
4444
close();
4545
},
4646
onError: (error: Error) => {

apps/web/src/components/(with-side-bar)/topic/remove-content-button.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export default function RemoveContentButton({
2828
},
2929
{
3030
onSuccess: () => {
31-
invalidateQueries([TOPIC.GET_TOPIC_BY_ID, topicId]);
31+
invalidateQueries([TOPIC.GET_TOPIC_BOARD_BY_ID, topicId]);
3232
setSelectedNodeIds([]);
3333
},
3434
onError: () => {

apps/web/src/components/(with-side-bar)/topic/remove-dialog-content.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Button, DialogClose, DialogContent, errorToast, useDialog } from "@link
44

55
interface RemoveDialogContentProps {
66
id: number | null;
7-
setIsDeleteModalOpen: (isOpen: boolean) => void;
7+
setIsDeleteModalOpen?: (isOpen: boolean) => void;
88
onDelete: (id: number) => Promise<void>;
99
}
1010

@@ -23,7 +23,7 @@ export default function RemoveDialogContent({
2323
};
2424

2525
useEffect(() => {
26-
setIsDeleteModalOpen(isOpen);
26+
setIsDeleteModalOpen?.(isOpen);
2727
}, [isOpen]);
2828

2929
return (

apps/web/src/components/(with-side-bar)/topic/sticker/block-note.tsx

Lines changed: 203 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,38 @@ import "@blocknote/core/fonts/inter.css";
44
import "@blocknote/shadcn/style.css";
55
import "@blocknote/core/fonts/inter.css";
66

7+
import { useEffect, useState } from "react";
8+
import { useRouter } from "next/navigation";
9+
10+
import { CUSTOM_STICKER } from "@/constants/custom-sticker";
11+
import { TOPIC } from "@/constants/topic";
12+
import { invalidateMany, invalidateQueries } from "@/lib/tanstack";
13+
import {
14+
useRemoveCustomSticker,
15+
useUpdateCustomSticker,
16+
} from "@/lib/tanstack/mutation/custom-sticker";
17+
import { useRemoveTopic, useUpdateTopic } from "@/lib/tanstack/mutation/topic";
18+
import { useGetCustomStickerById } from "@/lib/tanstack/query/custom-sticker";
19+
import { useGetTopicById } from "@/lib/tanstack/query/topic";
720
import { uploadImage } from "@/services/image";
21+
import { containsMarkdown } from "@/utils/markdown";
22+
import { revalidatePath } from "@/utils/revalidate";
823
import { defaultBlockSpecs } from "@blocknote/core";
924
import { BlockNoteSchema } from "@blocknote/core";
1025
import { ko } from "@blocknote/core/locales";
1126
import { useCreateBlockNote } from "@blocknote/react";
1227
import { BlockNoteView } from "@blocknote/shadcn";
28+
import { Button, Dialog, DialogTrigger, errorToast, successToast } from "@linkyboard/components";
29+
30+
import { Loader2, Save, Trash2 } from "lucide-react";
1331

1432
import ToggleBlock from "./toggle-block";
33+
import RemoveDialogContent from "../remove-dialog-content";
34+
35+
interface BlockNoteProps {
36+
topicId: string;
37+
stickerId: string;
38+
}
1539

1640
const blockNoteSchema = BlockNoteSchema.create({
1741
blockSpecs: {
@@ -28,16 +52,193 @@ const blockNoteSchema = BlockNoteSchema.create({
2852
},
2953
});
3054

31-
export default function BlockNote() {
55+
export default function BlockNote({ topicId, stickerId }: BlockNoteProps) {
56+
// stickerId가 있다면 커스텀 스티커(노란색) 조회
57+
const { data: topic, isLoading: isTopicLoading } = useGetTopicById({ id: topicId, stickerId });
58+
const { data: customSticker, isLoading: isCustomStickerLoading } =
59+
useGetCustomStickerById(stickerId);
60+
61+
const [title, setTitle] = useState(topic?.title || customSticker?.title || "");
62+
3263
const editor = useCreateBlockNote({
3364
schema: blockNoteSchema,
3465
dictionary: ko,
3566
uploadFile: uploadImage,
3667
});
3768

69+
const router = useRouter();
70+
71+
// 토픽 관련 hook
72+
const { mutateAsync: updateTopic, isPending: isUpdatePending } = useUpdateTopic();
73+
const { mutateAsync: removeTopic, isPending: isDeletePending } = useRemoveTopic();
74+
75+
// 스티커 관련 hook
76+
const { mutateAsync: updateCustomSticker, isPending: isUpdateCustomStickerPending } =
77+
useUpdateCustomSticker();
78+
const { mutateAsync: removeCustomSticker, isPending: isDeleteCustomStickerPending } =
79+
useRemoveCustomSticker();
80+
81+
const isCustomSticker = !!stickerId;
82+
const buttonDisabled =
83+
isUpdatePending ||
84+
isDeletePending ||
85+
isUpdateCustomStickerPending ||
86+
isDeleteCustomStickerPending;
87+
88+
const onSave = async () => {
89+
try {
90+
const content = editor.blocksToHTMLLossy();
91+
if (!isCustomSticker) {
92+
await updateTopic(
93+
{
94+
id: topicId,
95+
title,
96+
content,
97+
},
98+
{
99+
onSuccess: async () => {
100+
successToast("토픽이 성공적으로 수정되었어요.");
101+
await invalidateMany([
102+
[TOPIC.GET_ALL_TOPICS],
103+
[TOPIC.GET_TOPIC_BOARD_BY_ID, topicId],
104+
]);
105+
router.back();
106+
},
107+
}
108+
);
109+
} else {
110+
await updateCustomSticker(
111+
{
112+
customStickerId: stickerId,
113+
topicId,
114+
title,
115+
content,
116+
},
117+
{
118+
onSuccess: async () => {
119+
successToast("스티커가 성공적으로 수정되었어요.");
120+
await invalidateMany([
121+
[TOPIC.GET_TOPIC_BOARD_BY_ID, topicId],
122+
[CUSTOM_STICKER.GET_CUSTOM_STICKER_BY_ID, stickerId],
123+
]);
124+
router.back();
125+
},
126+
}
127+
);
128+
}
129+
} catch {
130+
errorToast("토픽 수정에 실패했어요.");
131+
}
132+
};
133+
134+
const onDelete = async () => {
135+
if (!isCustomSticker) {
136+
await removeTopic(topicId, {
137+
onSuccess: () => {
138+
successToast("토픽이 성공적으로 삭제되었어요.");
139+
invalidateQueries([TOPIC.GET_ALL_TOPICS]);
140+
revalidatePath(`/topic/${topicId}`);
141+
router.push("/topic");
142+
},
143+
onError: () => {
144+
errorToast("토픽 삭제에 실패했어요.");
145+
},
146+
});
147+
} else {
148+
await removeCustomSticker(stickerId, {
149+
onSuccess: () => {
150+
successToast("스티커가 성공적으로 삭제되었어요.");
151+
invalidateMany([
152+
[TOPIC.GET_TOPIC_BOARD_BY_ID, topicId],
153+
[CUSTOM_STICKER.GET_CUSTOM_STICKER_BY_ID, stickerId],
154+
]);
155+
router.back();
156+
},
157+
onError: () => {
158+
errorToast("스티커 삭제에 실패했어요.");
159+
},
160+
});
161+
}
162+
};
163+
164+
useEffect(() => {
165+
setTitle(topic?.title || customSticker?.title || "");
166+
167+
if (editor) {
168+
const content = topic?.content || customSticker?.content || "";
169+
170+
const loadContent = async () => {
171+
try {
172+
if (containsMarkdown(content)) {
173+
const blocks = await editor.tryParseMarkdownToBlocks(content);
174+
if (blocks) {
175+
editor.replaceBlocks(editor.document, blocks);
176+
}
177+
} else {
178+
const blocks = editor.tryParseHTMLToBlocks(content);
179+
if (blocks) {
180+
editor.replaceBlocks(editor.document, blocks);
181+
}
182+
}
183+
} catch (error) {
184+
console.error("에디터 내용 로드 실패:", error);
185+
editor.replaceBlocks(editor.document, []);
186+
}
187+
};
188+
189+
loadContent();
190+
}
191+
}, [editor, topic, customSticker]);
192+
38193
return (
39194
<div className="h-full">
40-
<BlockNoteView className="h-full" editor={editor} />
195+
{isTopicLoading || isCustomStickerLoading ? (
196+
<div className="flex h-full items-center justify-center">
197+
<Loader2 size={24} className="animate-spin" />
198+
</div>
199+
) : (
200+
<>
201+
<input
202+
className="border-border w-full border-b pb-3 text-3xl font-bold outline-none"
203+
placeholder="제목을 입력해주세요"
204+
value={title}
205+
onChange={(e) => setTitle(e.target.value)}
206+
/>
207+
<BlockNoteView className="h-[calc(100%-8rem)]" editor={editor} />
208+
<div className="flex gap-3">
209+
<Button
210+
onClick={onSave}
211+
className="h-12 flex-1 text-base"
212+
disabled={buttonDisabled || !title.trim()}
213+
>
214+
{isUpdatePending ? (
215+
<Loader2 size={18} className="mr-2 animate-spin" />
216+
) : (
217+
<>
218+
<Save size={18} className="mr-2" />
219+
저장
220+
</>
221+
)}
222+
</Button>
223+
<Dialog>
224+
<Button
225+
className="h-12 bg-red-400 text-base hover:bg-red-500"
226+
asChild
227+
disabled={buttonDisabled}
228+
>
229+
<DialogTrigger>
230+
<Trash2 size={18} className="mr-2" />
231+
삭제
232+
</DialogTrigger>
233+
</Button>
234+
<RemoveDialogContent
235+
id={Number(isCustomSticker ? stickerId : topicId)}
236+
onDelete={onDelete}
237+
/>
238+
</Dialog>
239+
</div>
240+
</>
241+
)}
41242
</div>
42243
);
43244
}

apps/web/src/components/(with-side-bar)/topic/sticker/remove-user-sticker-dialog.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export default function RemoveUserStickerDialog({
1818
const onRemoveTopic = async () => {
1919
await mutateAsync(customStickerId, {
2020
onSuccess: () => {
21-
invalidateQueries([TOPIC.GET_TOPIC_BY_ID, topicId]);
21+
invalidateQueries([TOPIC.GET_TOPIC_BOARD_BY_ID, topicId]);
2222
close();
2323
},
2424
onError: () => {

apps/web/src/components/(with-side-bar)/topic/sticker/topic-sticker-detail-modal.tsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
import { useRouter } from "next/navigation";
44

5-
import { useOutsideClick } from "@linkyboard/hooks";
6-
75
import { MoveDiagonal2, X } from "lucide-react";
86
import { createPortal } from "react-dom";
97

@@ -20,9 +18,11 @@ export default function TopicStickerDetailModal({
2018
}: TopicStickerDetailModalProps) {
2119
const router = useRouter();
2220

23-
const [modalRef] = useOutsideClick<HTMLDivElement>(() => {
24-
router.back();
25-
});
21+
const onMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
22+
if (e.currentTarget === e.target) {
23+
router.back();
24+
}
25+
};
2626

2727
const queryString = stickerId ? `?stickerId=${stickerId}` : "";
2828

@@ -40,8 +40,9 @@ export default function TopicStickerDetailModal({
4040
zIndex: 99999,
4141
backgroundColor: "rgba(0, 0, 0, 0.5)",
4242
}}
43+
onMouseDown={onMouseDown}
4344
>
44-
<div ref={modalRef} className="bg-background size-4/5 rounded-2xl p-8">
45+
<div className="bg-background size-4/5 rounded-2xl p-8">
4546
<div className="mb-4 flex items-center justify-between">
4647
{/* assign 외에 다른 방안 생각해보기 */}
4748
<button onClick={() => window.location.assign(`/topic/${id}/sticker${queryString}`)}>

apps/web/src/components/(with-side-bar)/topic/summarize-dialog.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ function SummarizeDialogContent({
8888
},
8989
{
9090
onSuccess: (data) => {
91-
invalidateQueries([TOPIC.GET_TOPIC_BY_ID, topicId]);
91+
invalidateQueries([TOPIC.GET_TOPIC_BOARD_BY_ID, topicId]);
9292
router.push(`/topic/${topicId}/sticker?stickerId=${data.result.id}`);
9393
reset();
9494
setSelectedNodeIds([]);
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export const CUSTOM_STICKER = {
22
GET_AI_MODELS: "get-ai-models",
3+
GET_CUSTOM_STICKER_BY_ID: "get-custom-sticker-by-id",
34
};

apps/web/src/constants/topic.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export const TOPIC = {
2+
GET_TOPIC_BOARD_BY_ID: "get-topic-board-by-id",
23
GET_TOPIC_BY_ID: "get-topic-by-id",
34
GET_ALL_TOPICS: "get-all-topics",
45
GET_ALL_CONTENTS: "get-all-contents",

apps/web/src/lib/tanstack/mutation/topic-content.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export const useCreateContent = (id: string) => {
3232
return useMutation({
3333
mutationFn: createContent,
3434
onSuccess: () => {
35-
invalidateQueries([TOPIC.GET_TOPIC_BY_ID, id]);
35+
invalidateQueries([TOPIC.GET_TOPIC_BOARD_BY_ID, id]);
3636
},
3737
onError: (error) => {
3838
const isDuplicate = error.message.includes("409");

0 commit comments

Comments
 (0)