Skip to content

Commit 0db37fc

Browse files
[myPage] fix : fix(임시저장): draft 중복 생성 방지 및 DRAFT→PUBLISHED 전환 처리
- feed/write: draftFeedIdRef로 feedId 추적, 재저장 시 updateFeed 사용 - feed/write: 등록하기 시 기존 draft는 updateFeed(PUBLISHED)로 전환 - feed/write: edit mode 수정하기에 status: PUBLISHED 추가 - qa/write: 등록 성공 시 localStorage draft 정리 추가 - my-posts: useGetMyBuilderFeedManagement({ status: DRAFT })로 교체 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 8d7bc20 commit 0db37fc

4 files changed

Lines changed: 318 additions & 232 deletions

File tree

src/app/(class-lesson)/class/[slug]/lesson/[id]/_components/lesson-qna-submission-modal.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -286,11 +286,11 @@ export function LessonQnaSubmissionModal({
286286
</div>
287287

288288
{/* Footer */}
289-
<div className="flex shrink-0 items-center justify-center gap-200 px-750 py-300">
289+
<div className="flex shrink-0 items-center gap-200 px-750 py-300">
290290
<button
291291
type="button"
292292
onClick={handleDraftSave}
293-
className="w-full rounded-100 border border-rose-400 px-400 py-200 font-designer-18b text-rose-500 hover:opacity-80"
293+
className="flex h-700 flex-1 items-center justify-center rounded-100 border border-rose-400 font-designer-18b text-rose-500 hover:opacity-80"
294294
>
295295
임시저장
296296
</button>
@@ -299,7 +299,7 @@ export function LessonQnaSubmissionModal({
299299
onClick={handleSubmit}
300300
disabled={createQna.isPending}
301301
className={cn(
302-
'w-full rounded-100 px-400 py-200 font-designer-18b text-text-inverse transition-opacity',
302+
'flex h-700 flex-1 items-center justify-center rounded-100 font-designer-18b text-text-inverse transition-opacity',
303303
createQna.isPending
304304
? 'cursor-not-allowed bg-gray-300'
305305
: 'bg-rose-500 hover:opacity-90',

src/app/(landing)/class/[slug]/(learning)/feed/write/page.tsx

Lines changed: 124 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ import {
1818
import { useToastStore } from '@/stores/use-toast-store';
1919
import { extractPlainTextFromHtml } from '@/utils/markdown-content';
2020

21+
const FEED_NOTICE = [
22+
'작성한 피드는 언제든지 수정하거나 삭제할 수 있습니다.',
23+
'다른 수강생에게 불쾌감을 줄 수 있는 내용은 운영 정책에 따라 삭제될 수 있습니다.',
24+
];
25+
2126
interface 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

Comments
 (0)