Skip to content

Commit 49f4b6f

Browse files
committed
feat: 스티커 삭제, 수정 API 연동
1 parent c10887d commit 49f4b6f

9 files changed

Lines changed: 184 additions & 40 deletions

File tree

apps/web/src/components/topic/edit-topic-sidebar/index.tsx

Lines changed: 79 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
"use client";
22

33
import { useEffect, useState } from "react";
4-
import { useRouter } from "next/navigation";
4+
import { useRouter, useSearchParams } from "next/navigation";
55

66
import Sidebar from "@/components/sidebar";
77
import { Button } from "@/components/ui/button";
88
import { Input } from "@/components/ui/input";
99
import { TOPIC } from "@/constants/topic";
1010
import { invalidateMany, invalidateQueries } from "@/lib/tanstack";
11+
import {
12+
useRemoveCustomSticker,
13+
useUpdateCustomSticker,
14+
} from "@/lib/tanstack/mutation/custom-sticker";
1115
import { useRemoveTopic, useUpdateTopic } from "@/lib/tanstack/mutation/topic";
1216
import { useTopicStore } from "@/lib/zustand/topic-store";
1317
import { containsMarkdown, markdownToHtml } from "@/utils/markdown";
@@ -29,16 +33,31 @@ import RemoveDialogContent from "../remove-dialog-content";
2933

3034
export default function EditTopicSidebar() {
3135
const router = useRouter();
36+
const searchParams = useSearchParams();
37+
const topicId = searchParams.get("id");
3238

3339
const [title, setTitle] = useState("");
3440
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
3541

42+
// 토픽 관련 hook
3643
const { mutateAsync: updateTopic, isPending: isUpdatePending } = useUpdateTopic();
3744
const { mutateAsync: removeTopic, isPending: isDeletePending } = useRemoveTopic();
45+
46+
// 스티커 관련 hook
47+
const { mutateAsync: updateCustomSticker, isPending: isUpdateCustomStickerPending } =
48+
useUpdateCustomSticker();
49+
const { mutateAsync: removeCustomSticker, isPending: isDeleteCustomStickerPending } =
50+
useRemoveCustomSticker();
51+
3852
const { setEditingTopic, setShowEditTopicSidebar, editingTopic, showEditTopicSidebar } =
3953
useTopicStore();
4054

41-
const buttonDisabled = isUpdatePending || isDeletePending;
55+
const buttonDisabled =
56+
isUpdatePending ||
57+
isDeletePending ||
58+
isUpdateCustomStickerPending ||
59+
isDeleteCustomStickerPending;
60+
const currentType = editingTopic?.type === "custom_sticker" ? "스티커" : "토픽";
4261

4362
const onClose = () => {
4463
setEditingTopic(null);
@@ -109,28 +128,42 @@ export default function EditTopicSidebar() {
109128
}, [editingTopic, editor]);
110129

111130
const onSave = async () => {
112-
if (!editingTopic || !editor) return errorToast("토픽 정보가 없어요.");
131+
if (!editingTopic || !editor) return errorToast(`${currentType} 정보가 없어요.`);
113132

114133
try {
115134
const content = editor.getHTML();
116135
// TODO: editingTopic.type에 따라 API 요청 다르게
117-
await updateTopic(
118-
{
119-
id: editingTopic.id,
120-
title,
121-
content,
122-
},
123-
{
124-
onSuccess: async () => {
125-
successToast("토픽이 성공적으로 수정되었어요.");
126-
await invalidateMany([
127-
[TOPIC.GET_ALL_TOPICS],
128-
[TOPIC.GET_TOPIC_BY_ID, editingTopic?.id.toString()],
129-
]);
130-
onClose();
136+
if (editingTopic.type === "topic") {
137+
await updateTopic(
138+
{
139+
id: editingTopic.id,
140+
title,
141+
content,
131142
},
132-
}
133-
);
143+
{
144+
onSuccess: async () => {
145+
successToast("토픽이 성공적으로 수정되었어요.");
146+
await invalidateMany([[TOPIC.GET_ALL_TOPICS], [TOPIC.GET_TOPIC_BY_ID, topicId]]);
147+
onClose();
148+
},
149+
}
150+
);
151+
} else if (editingTopic.type === "custom_sticker") {
152+
await updateCustomSticker(
153+
{
154+
customStickerId: editingTopic.id,
155+
title,
156+
content,
157+
},
158+
{
159+
onSuccess: async () => {
160+
successToast("스티커가 성공적으로 수정되었어요.");
161+
invalidateQueries([TOPIC.GET_TOPIC_BY_ID, topicId]);
162+
onClose();
163+
},
164+
}
165+
);
166+
}
134167
} catch {
135168
errorToast("토픽 수정에 실패했어요.");
136169
}
@@ -143,26 +176,39 @@ export default function EditTopicSidebar() {
143176
};
144177

145178
const onDelete = async (id: number) => {
146-
await removeTopic(id, {
147-
onSuccess: () => {
148-
successToast("토픽이 성공적으로 삭제되었어요.");
149-
onCloseSidebar();
150-
invalidateQueries([TOPIC.GET_ALL_TOPICS]);
151-
revalidatePath(`/topic?id=${id}`);
152-
router.back();
153-
},
154-
onError: () => {
155-
errorToast("토픽 삭제에 실패했어요.");
156-
},
157-
});
179+
if (editingTopic?.type === "topic") {
180+
await removeTopic(id, {
181+
onSuccess: () => {
182+
successToast("토픽이 성공적으로 삭제되었어요.");
183+
onCloseSidebar();
184+
invalidateQueries([TOPIC.GET_ALL_TOPICS]);
185+
revalidatePath(`/topic?id=${id}`);
186+
router.back();
187+
},
188+
onError: () => {
189+
errorToast("토픽 삭제에 실패했어요.");
190+
},
191+
});
192+
} else if (editingTopic?.type === "custom_sticker") {
193+
await removeCustomSticker(id, {
194+
onSuccess: () => {
195+
successToast("스티커가 성공적으로 삭제되었어요.");
196+
onCloseSidebar();
197+
invalidateQueries([TOPIC.GET_ALL_TOPICS]);
198+
},
199+
onError: () => {
200+
errorToast("스티커 삭제에 실패했어요.");
201+
},
202+
});
203+
}
158204
};
159205

160206
return (
161207
<Sidebar isOpen={showEditTopicSidebar} onClose={onClickOutside}>
162208
<div className="flex h-full flex-col">
163209
{/* 헤더 */}
164210
<div className="flex items-center justify-between border-b p-6">
165-
<h2 className="text-xl font-semibold">토픽 편집</h2>
211+
<h2 className="text-xl font-semibold">{currentType} 편집</h2>
166212
<Button variant="ghost" size="icon" onClick={onCloseSidebar} aria-label="사이드바 닫기">
167213
<X size={24} />
168214
</Button>
@@ -176,7 +222,7 @@ export default function EditTopicSidebar() {
176222
<Input
177223
value={title}
178224
onChange={(e) => setTitle(e.target.value)}
179-
placeholder="토픽 제목을 입력하세요"
225+
placeholder={`${currentType} 제목을 입력하세요`}
180226
className="text-base"
181227
/>
182228
</div>
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { TOPIC } from "@/constants/topic";
2+
import { invalidateQueries } from "@/lib/tanstack";
3+
import { useRemoveCustomSticker } from "@/lib/tanstack/mutation/custom-sticker";
4+
import { errorToast, successToast } from "@/utils/toast";
5+
import { DialogClose, DialogContent, useDialog } from "@repo/ui/components/dialog";
6+
7+
import { Loader2 } from "lucide-react";
8+
9+
import { Button } from "../ui/button";
10+
11+
export default function RemoveUserStickerDialog({ topicId }: { topicId: number }) {
12+
const { mutateAsync, isPending } = useRemoveCustomSticker();
13+
const { close } = useDialog();
14+
15+
const onRemoveTopic = async () => {
16+
await mutateAsync(topicId, {
17+
onSuccess: () => {
18+
successToast("스티커가 삭제되었어요.");
19+
invalidateQueries([TOPIC.GET_TOPIC_BY_ID, topicId.toString()]);
20+
close();
21+
},
22+
onError: () => {
23+
errorToast("스티커 삭제에 실패했어요.");
24+
},
25+
});
26+
};
27+
28+
return (
29+
<DialogContent>
30+
<div className="space-y-4">
31+
<div>
32+
<h2 className="text-foreground text-lg font-semibold">삭제</h2>
33+
<p className="text-muted-foreground text-sm">스티커를 삭제 하시겠습니까?</p>
34+
</div>
35+
<div className="flex justify-end gap-2">
36+
<Button variant="outline" asChild disabled={isPending}>
37+
<DialogClose>취소</DialogClose>
38+
</Button>
39+
<Button onClick={onRemoveTopic} disabled={isPending}>
40+
{isPending ? <Loader2 size={16} className="animate-spin" /> : "삭제"}
41+
</Button>
42+
</div>
43+
</div>
44+
</DialogContent>
45+
);
46+
}

apps/web/src/components/topic/summarize-dialog.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ function SummarizeDialogContent({ topicId, selectedNodeIds }: SummarizeDialogPro
7979
invalidateQueries([TOPIC.GET_TOPIC_BY_ID, topicId]);
8080
setEditingTopic({
8181
...data.result,
82+
content: data.result.draftMd,
8283
type: "custom_sticker",
8384
});
8485
setShowEditTopicSidebar(true);

apps/web/src/components/topic/user-sticker.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { Dialog, DialogTrigger } from "@repo/ui/components/dialog";
55

66
import { Edit, Sticker, Trash2 } from "lucide-react";
77

8-
import RemoveTopicDialog from "./remove-topic-dialog";
8+
import RemoveUserStickerDialog from "./remove-user-sticker-dialog";
99
import { Button } from "../ui/button";
1010

1111
export default function UserSticker({ item }: { item: TopicDTO }) {
@@ -53,7 +53,7 @@ export default function UserSticker({ item }: { item: TopicDTO }) {
5353
<Trash2 size={16} />
5454
</DialogTrigger>
5555
</Button>
56-
<RemoveTopicDialog topicId={item.id} />
56+
<RemoveUserStickerDialog topicId={item.id} />
5757
</Dialog>
5858
</div>
5959
</div>

apps/web/src/constants/topic.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
export const TOPIC = {
22
GET_TOPIC_BY_ID: "get-topic-by-id",
33
GET_ALL_TOPICS: "get-all-topics",
4-
GET_TOPIC_CONTENT_BY_ID: "get-topic-content-by-id",
54
GET_ALL_CONTENTS: "get-all-contents",
65
};

apps/web/src/lib/tanstack/mutation/custom-sticker.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import {
2+
createCustomSticker,
3+
removeCustomSticker,
24
summarizeTopicContent,
5+
updateCustomSticker,
36
updateCustomStickerPosition,
47
updateCustomStickerSize,
58
} from "@/services/custom-sticker";
@@ -22,3 +25,21 @@ export const useUpdateCustomStickerSize = () => {
2225
mutationFn: updateCustomStickerSize,
2326
});
2427
};
28+
29+
export const useRemoveCustomSticker = () => {
30+
return useMutation({
31+
mutationFn: removeCustomSticker,
32+
});
33+
};
34+
35+
export const useCreateCustomSticker = () => {
36+
return useMutation({
37+
mutationFn: createCustomSticker,
38+
});
39+
};
40+
41+
export const useUpdateCustomSticker = () => {
42+
return useMutation({
43+
mutationFn: updateCustomSticker,
44+
});
45+
};

apps/web/src/models/topic.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ export interface TopicDTO {
88
content: string;
99
}
1010

11+
export interface SummarizeContentDTO {
12+
id: number;
13+
title: string;
14+
draftMd: string;
15+
}
16+
1117
export interface TopicDetailDTO {
1218
nodes: {
1319
data: {

apps/web/src/page/topic/index.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3+
import { Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react";
44

55
import ContentList from "@/components/topic/content-list";
66
import CustomNode from "@/components/topic/custom-node";
@@ -308,7 +308,9 @@ export default function TopicBoardPage({ id, type }: TopicBoardPageProps) {
308308
</div>
309309

310310
{/* Edit Topic Sidebar */}
311-
<EditTopicSidebar />
311+
<Suspense>
312+
<EditTopicSidebar />
313+
</Suspense>
312314
</div>
313315
);
314316
}

apps/web/src/services/custom-sticker.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { AIModelDTO } from "@/models/custom-sticker";
2-
import { TopicDTO } from "@/models/topic";
2+
import { SummarizeContentDTO } from "@/models/topic";
33
import { BaseResponseDTO } from "@repo/types";
44

55
import { clientApi } from ".";
@@ -13,7 +13,7 @@ export const summarizeTopicContent = async (props: {
1313
selectedContentIds: number[];
1414
requirements: string;
1515
modelAlias: string;
16-
}): Promise<BaseResponseDTO<TopicDTO>> => {
16+
}): Promise<BaseResponseDTO<SummarizeContentDTO>> => {
1717
return clientApi.post(`custom-stickers/summarize`, props);
1818
};
1919

@@ -34,3 +34,26 @@ export const updateCustomStickerSize = async (props: {
3434
const { customStickerId, ...restProps } = props;
3535
return clientApi.put(`custom-stickers/${customStickerId}/resize`, restProps);
3636
};
37+
38+
export const removeCustomSticker = async (
39+
customStickerId: number
40+
): Promise<BaseResponseDTO<unknown>> => {
41+
return clientApi.delete(`custom-stickers/${customStickerId}`);
42+
};
43+
44+
export const createCustomSticker = async (props: {
45+
topicId: number;
46+
title: string;
47+
content: string;
48+
}): Promise<BaseResponseDTO<unknown>> => {
49+
return clientApi.post(`custom-stickers`, props);
50+
};
51+
52+
export const updateCustomSticker = async (props: {
53+
customStickerId: number;
54+
title: string;
55+
content: string;
56+
}): Promise<BaseResponseDTO<unknown>> => {
57+
const { customStickerId, ...restProps } = props;
58+
return clientApi.put(`custom-stickers/${customStickerId}`, restProps);
59+
};

0 commit comments

Comments
 (0)