@@ -18,6 +18,11 @@ import {
1818import { useToastStore } from '@/stores/use-toast-store' ;
1919import { extractPlainTextFromHtml } from '@/utils/markdown-content' ;
2020
21+ const FEED_NOTICE = [
22+ '작성한 피드는 언제든지 수정하거나 삭제할 수 있습니다.' ,
23+ '다른 수강생에게 불쾌감을 줄 수 있는 내용은 운영 정책에 따라 삭제될 수 있습니다.' ,
24+ ] ;
25+
2126interface AttachedImage {
2227 previewUrl : string ;
2328 key : string ;
@@ -40,11 +45,11 @@ export default function FeedWritePage() {
4045 const [ selectedLessonId , setSelectedLessonId ] = useState < number | null > ( null ) ;
4146 const [ lessonOpen , setLessonOpen ] = useState ( false ) ;
4247 const [ text , setText ] = useState ( '' ) ;
43- const [ showCancelModal , setShowCancelModal ] = useState ( false ) ;
4448 const [ images , setImages ] = useState < AttachedImage [ ] > ( [ ] ) ;
4549 const [ isUploadingImage , setIsUploadingImage ] = useState ( false ) ;
4650 const [ initialized , setInitialized ] = useState ( false ) ;
4751 const fileInputRef = useRef < HTMLInputElement > ( null ) ;
52+ const draftFeedIdRef = useRef < number | null > ( null ) ;
4853
4954 const { data : courseData } = useGetCourseDetail ( slug ) ;
5055 const courseId = courseData ?. courseId ?? 0 ;
@@ -66,6 +71,20 @@ export default function FeedWritePage() {
6671 setInitialized ( true ) ;
6772 } , [ existingFeed , initialized , isEditMode ] ) ;
6873
74+ useEffect ( ( ) => {
75+ if ( isEditMode ) return ;
76+ const draft = localStorage . getItem ( `course-feed-draft-${ slug } ` ) ;
77+ if ( ! draft ) return ;
78+ try {
79+ const { content : c , lessonId : l , feedId : f } = JSON . parse ( draft ) ;
80+ if ( c ) setText ( c ) ;
81+ if ( l ) setSelectedLessonId ( l ) ;
82+ if ( typeof f === 'number' ) draftFeedIdRef . current = f ;
83+ } catch {
84+ // malformed draft — ignore
85+ }
86+ } , [ slug , isEditMode ] ) ;
87+
6988 const allLessons =
7089 curriculum ?. chapters . flatMap ( ( ch ) =>
7190 ch . lessons . map ( ( l ) => ( {
@@ -107,6 +126,57 @@ export default function FeedWritePage() {
107126 } ) ;
108127 }
109128
129+ function handleSaveDraft ( ) {
130+ if ( ! selectedLessonId ) {
131+ showToast ( '레슨을 선택해주세요.' , 'error' ) ;
132+ return ;
133+ }
134+ if ( ! extractPlainTextFromHtml ( text ) ) {
135+ showToast ( '내용을 입력해주세요.' , 'error' ) ;
136+ return ;
137+ }
138+ const persistDraft = ( feedId : number ) => {
139+ draftFeedIdRef . current = feedId ;
140+ localStorage . setItem (
141+ `course-feed-draft-${ slug } ` ,
142+ JSON . stringify ( { content : text , lessonId : selectedLessonId , feedId } ) ,
143+ ) ;
144+ showToast ( '임시저장되었어요.' ) ;
145+ } ;
146+ if ( draftFeedIdRef . current ) {
147+ updateFeed . mutate (
148+ {
149+ feedId : draftFeedIdRef . current ,
150+ request : {
151+ content : text ,
152+ imageKeys : images . map ( ( img ) => img . key ) ,
153+ status : 'DRAFT' ,
154+ } ,
155+ } ,
156+ {
157+ onSuccess : ( ) => persistDraft ( draftFeedIdRef . current ! ) ,
158+ onError : ( ) => showToast ( '임시저장에 실패했어요.' , 'error' ) ,
159+ } ,
160+ ) ;
161+ } else {
162+ createFeed . mutate (
163+ {
164+ courseId,
165+ request : {
166+ lessonId : selectedLessonId ,
167+ content : text ,
168+ imageKeys : images . map ( ( img ) => img . key ) ,
169+ status : 'DRAFT' ,
170+ } ,
171+ } ,
172+ {
173+ onSuccess : ( data ) => persistDraft ( data . feedId ) ,
174+ onError : ( ) => showToast ( '임시저장에 실패했어요.' , 'error' ) ,
175+ } ,
176+ ) ;
177+ }
178+ }
179+
110180 function handleSubmit ( ) {
111181 if ( isEditMode ) {
112182 if ( ! extractPlainTextFromHtml ( text ) ) {
@@ -119,6 +189,7 @@ export default function FeedWritePage() {
119189 request : {
120190 content : text ,
121191 imageKeys : images . map ( ( i ) => i . key ) ,
192+ status : 'PUBLISHED' ,
122193 } ,
123194 } ,
124195 {
@@ -138,23 +209,42 @@ export default function FeedWritePage() {
138209 showToast ( '내용을 입력해주세요.' , 'error' ) ;
139210 return ;
140211 }
141- createFeed . mutate (
142- {
143- courseId,
144- request : {
145- lessonId : selectedLessonId ,
146- content : text ,
147- imageKeys : images . map ( ( img ) => img . key ) ,
212+ const onPublishSuccess = ( ) => {
213+ localStorage . removeItem ( `course-feed-draft-${ slug } ` ) ;
214+ showToast ( '피드가 등록되었어요!' ) ;
215+ router . push ( `/class/${ slug } /home?tab=feed` ) ;
216+ } ;
217+ if ( draftFeedIdRef . current ) {
218+ updateFeed . mutate (
219+ {
220+ feedId : draftFeedIdRef . current ,
221+ request : {
222+ content : text ,
223+ imageKeys : images . map ( ( img ) => img . key ) ,
224+ status : 'PUBLISHED' ,
225+ } ,
148226 } ,
149- } ,
150- {
151- onSuccess : ( ) => {
152- showToast ( '피드가 등록되었어요!' ) ;
153- router . push ( `/class/${ slug } /home?tab=feed` ) ;
227+ {
228+ onSuccess : onPublishSuccess ,
229+ onError : ( ) => showToast ( '등록에 실패했어요.' , 'error' ) ,
154230 } ,
155- onError : ( ) => showToast ( '등록에 실패했어요.' , 'error' ) ,
156- } ,
157- ) ;
231+ ) ;
232+ } else {
233+ createFeed . mutate (
234+ {
235+ courseId,
236+ request : {
237+ lessonId : selectedLessonId ,
238+ content : text ,
239+ imageKeys : images . map ( ( img ) => img . key ) ,
240+ } ,
241+ } ,
242+ {
243+ onSuccess : onPublishSuccess ,
244+ onError : ( ) => showToast ( '등록에 실패했어요.' , 'error' ) ,
245+ } ,
246+ ) ;
247+ }
158248 }
159249 }
160250
@@ -165,41 +255,10 @@ export default function FeedWritePage() {
165255 const submitLabel = isEditMode ? '수정하기' : '등록하기' ;
166256 const submitPending = isEditMode
167257 ? updateFeed . isPending
168- : createFeed . isPending ;
258+ : createFeed . isPending || updateFeed . isPending ;
169259
170260 return (
171261 < >
172- { /* Cancel confirmation modal — create mode only */ }
173- { showCancelModal && (
174- < div className = "fixed inset-0 z-50 flex items-center justify-center bg-black/40" >
175- < div className = "flex w-5000 flex-col items-center gap-300 rounded-200 bg-background-default p-500" >
176- < div className = "text-center" >
177- < p className = "font-designer-20b text-gray-800" >
178- 피드 등록을 취소하시겠습니까?
179- </ p >
180- < p className = "mt-150 font-designer-16r text-gray-500" >
181- 작성된 내용은 저장되지 않습니다.
182- </ p >
183- </ div >
184- < div className = "flex w-full gap-200" >
185- < button
186- type = "button"
187- onClick = { ( ) => setShowCancelModal ( false ) }
188- className = "flex h-700 flex-1 items-center justify-center rounded-100 border border-border-default font-designer-16m text-gray-800"
189- >
190- 계속 작성
191- </ button >
192- < Link
193- href = { `/class/${ slug } /home?tab=feed` }
194- className = "flex h-700 flex-1 items-center justify-center rounded-100 bg-background-brand-default font-designer-16m text-text-inverse"
195- >
196- 확인
197- </ Link >
198- </ div >
199- </ div >
200- </ div >
201- ) }
202-
203262 < div className = "w-full pb-800" >
204263 < div className = "mx-auto max-w-page px-600 pt-500" >
205264 < Link
@@ -293,7 +352,7 @@ export default function FeedWritePage() {
293352 { images . map ( ( img , i ) => (
294353 < div
295354 key = { img . key }
296- className = "relative h-1625 w-1625 shrink-0"
355+ className = "relative h-1500 w-1500 shrink-0"
297356 >
298357 < Image
299358 src = { img . previewUrl }
@@ -318,7 +377,7 @@ export default function FeedWritePage() {
318377 disabled = { isUploadingImage }
319378 onClick = { ( ) => fileInputRef . current ?. click ( ) }
320379 className = { cn (
321- 'flex h-1625 w-1625 shrink-0 flex-col items-center justify-center gap-75 rounded-150 border border-border-default bg-gray-200' ,
380+ 'flex h-1500 w-1500 shrink-0 flex-col items-center justify-center gap-75 rounded-150 border border-border-default bg-gray-200' ,
322381 isUploadingImage
323382 ? 'cursor-not-allowed opacity-50'
324383 : 'hover:border-rose-400' ,
@@ -354,6 +413,16 @@ export default function FeedWritePage() {
354413 uploadImage = { uploadCommunityMarkdownImage }
355414 />
356415
416+ { /* Notice */ }
417+ < div className = "rounded-200 border border-gray-200 p-200" >
418+ < p className = "mb-100 font-designer-16m text-gray-400" > 유의사항</ p >
419+ < ul className = "list-disc space-y-75 pl-300 font-designer-13r text-gray-400" >
420+ { FEED_NOTICE . map ( ( item ) => (
421+ < li key = { item } > { item } </ li >
422+ ) ) }
423+ </ ul >
424+ </ div >
425+
357426 { /* CTAs */ }
358427 < div className = "flex gap-200" >
359428 { isEditMode ? (
@@ -362,15 +431,16 @@ export default function FeedWritePage() {
362431 onClick = { ( ) =>
363432 router . push ( `/class/${ slug } /feed/${ editFeedId } ` )
364433 }
365- className = "flex h-700 flex-1 items-center justify-center rounded-100 border border-border-default font-designer-16m text-gray-800"
434+ className = "flex h-775 flex-1 items-center justify-center rounded-100 border border-border-default font-designer-16m text-gray-800"
366435 >
367436 취소
368437 </ button >
369438 ) : (
370439 < button
371440 type = "button"
372- onClick = { ( ) => setShowCancelModal ( true ) }
373- className = "flex h-700 flex-1 items-center justify-center rounded-100 border border-border-default font-designer-16m text-gray-800"
441+ onClick = { handleSaveDraft }
442+ disabled = { createFeed . isPending || updateFeed . isPending }
443+ className = "flex h-775 flex-1 items-center justify-center rounded-100 border border-rose-400 font-designer-16m text-rose-500 disabled:opacity-50"
374444 >
375445 임시저장
376446 </ button >
@@ -379,7 +449,7 @@ export default function FeedWritePage() {
379449 type = "button"
380450 onClick = { handleSubmit }
381451 disabled = { submitPending }
382- className = "flex h-700 flex-1 items-center justify-center rounded-100 bg-background-brand-default font-designer-16m text-text-inverse disabled:bg-gray-300"
452+ className = "flex h-775 flex-1 items-center justify-center rounded-100 bg-background-brand-default font-designer-16m text-text-inverse disabled:bg-gray-300"
383453 >
384454 { submitLabel }
385455 </ button >
0 commit comments