@@ -4,14 +4,38 @@ import "@blocknote/core/fonts/inter.css";
44import "@blocknote/shadcn/style.css" ;
55import "@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" ;
720import { uploadImage } from "@/services/image" ;
21+ import { containsMarkdown } from "@/utils/markdown" ;
22+ import { revalidatePath } from "@/utils/revalidate" ;
823import { defaultBlockSpecs } from "@blocknote/core" ;
924import { BlockNoteSchema } from "@blocknote/core" ;
1025import { ko } from "@blocknote/core/locales" ;
1126import { useCreateBlockNote } from "@blocknote/react" ;
1227import { BlockNoteView } from "@blocknote/shadcn" ;
28+ import { Button , Dialog , DialogTrigger , errorToast , successToast } from "@linkyboard/components" ;
29+
30+ import { Loader2 , Save , Trash2 } from "lucide-react" ;
1331
1432import ToggleBlock from "./toggle-block" ;
33+ import RemoveDialogContent from "../remove-dialog-content" ;
34+
35+ interface BlockNoteProps {
36+ topicId : string ;
37+ stickerId : string ;
38+ }
1539
1640const 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}
0 commit comments