Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions src/app/admin/blog/magnet/[id]/post/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
// TODO: 마그넷 글 관리 페이지 구현 예정
export default function MagnetPostPage() {
return <div>글 관리 (준비 중)</div>;
}
import { fetchMagnetPost } from '@/domain/admin/blog/magnet/mock';
import MagnetPostPage from '@/domain/admin/blog/magnet/MagnetPostPage';

const Page = async ({ params }: { params: Promise<{ id: string }> }) => {
const { id } = await params;
const initialData = await fetchMagnetPost(Number(id));

return <MagnetPostPage magnetId={id} initialData={initialData} />;
};

export default Page;
173 changes: 173 additions & 0 deletions src/domain/admin/blog/magnet/MagnetPostPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
'use client';

import TextFieldLimit from '@/domain/admin/blog/TextFieldLimit';
import { useMagnetPostForm } from '@/domain/admin/blog/magnet/hooks/useMagnetPostForm';
import MagnetProgramRecommendSection from '@/domain/admin/blog/magnet/section/MagnetProgramRecommendSection';
import MagnetRecommendSection from '@/domain/admin/blog/magnet/section/MagnetRecommendSection';
import { MAGNET_TYPE, MagnetPostDetail } from '@/domain/admin/blog/magnet/types';
import Heading from '@/domain/admin/ui/heading/Heading';
import Heading2 from '@/domain/admin/ui/heading/Heading2';
import ImageUpload from '@/domain/admin/program/ui/form/ImageUpload';
import { Button, Checkbox, FormControlLabel } from '@mui/material';
import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker';
import dynamic from 'next/dynamic';

const EditorApp = dynamic(
() => import('@/domain/admin/lexical/EditorApp'),
{ ssr: false },
);

const MAX_META_DESCRIPTION_LENGTH = 100;

interface MagnetPostPageProps {
magnetId: string;
initialData: MagnetPostDetail;
}

const MagnetPostPage = ({ magnetId, initialData }: MagnetPostPageProps) => {
const {
type,
title,
formState,
displayDate,
endDate,
content,
initialEditorStateBefore,
initialEditorStateAfter,
onChangeMetaDescription,
onChangeThumbnailFile,
onChangeHasCommonForm,
onChangeProgramRecommend,
onChangeMagnetRecommend,
onChangeEditorBefore,
onChangeEditorAfter,
setDisplayDate,
setEndDate,
savePost,
navigateToList,
} = useMagnetPostForm({ magnetId, initialData });

return (
<div className="mx-6 mb-40 mt-6">
<header className="mb-4">
<Heading>마그넷 글 관리</Heading>
</header>
<main className="max-w-screen-xl">
<div className="flex flex-col gap-6">
{/* 4.1 타입 */}
<p className="text-lg font-medium">
타입: &nbsp;{MAGNET_TYPE[type]}
</p>

{/* 4.2 제목 */}
<p className="text-lg font-medium">
제목: &nbsp;{title}
</p>

{/* 4.3 메타 디스크립션 */}
<TextFieldLimit
type="text"
label="메타 디스크립션"
placeholder="메타 디스크립션"
name="metaDescription"
value={formState.metaDescription}
onChange={onChangeMetaDescription}
multiline
minRows={3}
fullWidth
maxLength={MAX_META_DESCRIPTION_LENGTH}
/>

{/* 4.4 썸네일 */}
<div className="w-72">
<ImageUpload
label="썸네일 등록"
id="magnet-thumbnail"
image={formState.thumbnail}
onChange={onChangeThumbnailFile}
/>
</div>

{/* 4.5 프로그램 추천 + 4.6 마그넷 추천 */}
<div className="flex gap-5">
<MagnetProgramRecommendSection
programRecommend={content.programRecommend}
onChangeProgramRecommend={onChangeProgramRecommend}
/>
<MagnetRecommendSection
magnetRecommend={content.magnetRecommend}
onChangeMagnetRecommend={onChangeMagnetRecommend}
/>
</div>

{/* 4.7 노출 기간 */}
<div className="border px-6 py-10">
<Heading2 className="mb-4">노출 기간</Heading2>
<div className="flex gap-4">
<DateTimePicker
label="시작 일자"
value={displayDate}
onChange={setDisplayDate}
format="YYYY.MM.DD(dd) HH:mm"
ampm={false}
/>
<DateTimePicker
label="종료 일자"
value={endDate}
onChange={setEndDate}
format="YYYY.MM.DD(dd) HH:mm"
ampm={false}
/>
</div>
</div>

{/* 4.8 공통 신청폼 추가 */}
<FormControlLabel
control={
<Checkbox
checked={formState.hasCommonForm}
onChange={(e) => onChangeHasCommonForm(e.target.checked)}
/>
}
label="공통 신청폼 추가"
/>

{/* 4.9 콘텐츠 편집1 (신청 전 공개) */}
<div>
<Heading2 className="mb-2">콘텐츠 편집1(신청 전 공개)</Heading2>
<EditorApp
initialEditorStateJsonString={initialEditorStateBefore}
onChange={onChangeEditorBefore}
/>
</div>

{/* 4.10 콘텐츠 편집2 (신청 후 공개) */}
<div>
<Heading2 className="mb-2">콘텐츠 편집2(신청 후 공개)</Heading2>
<EditorApp
initialEditorStateJsonString={initialEditorStateAfter}
onChange={onChangeEditorAfter}
/>
</div>

{/* 4.11 액션 버튼 */}
<div className="flex items-center justify-end gap-4">
<Button variant="outlined" type="button" onClick={navigateToList}>
취소 (리스트로 돌아가기)
</Button>
<Button
variant="contained"
color="primary"
type="button"
onClick={savePost}
>
등록하기
</Button>
</div>
</div>
</main>
</div>
);
};

export default MagnetPostPage;
153 changes: 153 additions & 0 deletions src/domain/admin/blog/magnet/hooks/useMagnetPostForm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { uploadFile } from '@/api/file';
import { saveMagnetPost } from '@/domain/admin/blog/magnet/mock';
import {
MagnetPostContent,
MagnetPostDetail,
MagnetProgramRecommendItem,
} from '@/domain/admin/blog/magnet/types';
import { useAdminSnackbar } from '@/hooks/useAdminSnackbar';
import dayjs from '@/lib/dayjs';
import { Dayjs } from 'dayjs';
import { useRouter } from 'next/navigation';
import { ChangeEvent, useMemo, useState } from 'react';

const RECOMMEND_SLOT_COUNT = 4;

function createEmptyContent(): MagnetPostContent {
return {
programRecommend: Array.from({ length: RECOMMEND_SLOT_COUNT }, () => ({
id: null,
})),
magnetRecommend: Array.from({ length: RECOMMEND_SLOT_COUNT }, () => null),
};
}

function parseInitialContent(data: MagnetPostDetail): MagnetPostContent {
if (!data.content || data.content === '') return createEmptyContent();
try {
return JSON.parse(data.content);
} catch {
return createEmptyContent();
}
}

interface FormState {
metaDescription: string;
thumbnail: string;
hasCommonForm: boolean;
}

function buildInitialFormState(data: MagnetPostDetail): FormState {
return {
metaDescription: data.metaDescription ?? '',
thumbnail: data.thumbnail ?? '',
hasCommonForm: data.hasCommonForm ?? false,
};
}

interface UseMagnetPostFormParams {
magnetId: string;
initialData: MagnetPostDetail;
}

export const useMagnetPostForm = ({
magnetId,
initialData,
}: UseMagnetPostFormParams) => {
const router = useRouter();
const { snackbar: setSnackbar } = useAdminSnackbar();

const initialContent = useMemo(
() => parseInitialContent(initialData),
[initialData],
);
const initialFormState = useMemo(
() => buildInitialFormState(initialData),
[initialData],
);

const [formState, setFormState] = useState<FormState>(initialFormState);
const [displayDate, setDisplayDate] = useState<Dayjs | null>(
initialData.displayDate ? dayjs(initialData.displayDate) : null,
);
const [endDate, setEndDate] = useState<Dayjs | null>(
initialData.endDate ? dayjs(initialData.endDate) : null,
);
const [content, setContent] = useState<MagnetPostContent>(initialContent);

const onChangeMetaDescription = (e: ChangeEvent<HTMLInputElement>) => {
setFormState((prev) => ({ ...prev, metaDescription: e.target.value }));
};

const onChangeThumbnailFile = async (
e: ChangeEvent<HTMLInputElement>,
) => {
const file = e.target.files?.item(0);
if (!file) {
setSnackbar('파일이 없습니다.');
return;
}
const url = await uploadFile({ file, type: 'BLOG' });
setFormState((prev) => ({ ...prev, thumbnail: url }));
};

const onChangeHasCommonForm = (checked: boolean) => {
setFormState((prev) => ({ ...prev, hasCommonForm: checked }));
};

const onChangeProgramRecommend = (items: MagnetProgramRecommendItem[]) => {
setContent((prev) => ({ ...prev, programRecommend: items }));
};

const onChangeMagnetRecommend = (items: (number | null)[]) => {
setContent((prev) => ({ ...prev, magnetRecommend: items }));
};

const onChangeEditorBefore = (jsonString: string) => {
setContent((prev) => ({ ...prev, lexicalBefore: jsonString }));
};

const onChangeEditorAfter = (jsonString: string) => {
setContent((prev) => ({ ...prev, lexicalAfter: jsonString }));
};

const savePost = async () => {
saveMagnetPost({
magnetId: Number(magnetId),
metaDescription: formState.metaDescription,
thumbnail: formState.thumbnail,
displayDate: displayDate?.format('YYYY-MM-DDTHH:mm') ?? null,
endDate: endDate?.format('YYYY-MM-DDTHH:mm') ?? null,
hasCommonForm: formState.hasCommonForm,
content: JSON.stringify(content),
isVisible: false,
});
setSnackbar('마그넷 글이 저장되었습니다.');
};

const navigateToList = () => {
router.push('/admin/blog/magnet/list');
};

return {
type: initialData.type,
title: initialData.title,
formState,
displayDate,
endDate,
content,
initialEditorStateBefore: initialContent.lexicalBefore,
initialEditorStateAfter: initialContent.lexicalAfter,
onChangeMetaDescription,
onChangeThumbnailFile,
onChangeHasCommonForm,
onChangeProgramRecommend,
onChangeMagnetRecommend,
onChangeEditorBefore,
onChangeEditorAfter,
setDisplayDate,
setEndDate,
savePost,
navigateToList,
};
};
Loading