Skip to content

Commit e3aee2c

Browse files
authored
Merge pull request #2131 from Let-s-intern/LC-2856-ADMIN-마그넷-내부화-글-등록
LC-2856 ADMIN 마그넷 내부화 글 등록
2 parents 278fc9c + 1b9b495 commit e3aee2c

7 files changed

Lines changed: 676 additions & 5 deletions

File tree

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
// TODO: 마그넷 글 관리 페이지 구현 예정
2-
export default function MagnetPostPage() {
3-
return <div>글 관리 (준비 중)</div>;
4-
}
1+
import { fetchMagnetPost } from '@/domain/admin/blog/magnet/mock';
2+
import MagnetPostPage from '@/domain/admin/blog/magnet/MagnetPostPage';
3+
4+
const Page = async ({ params }: { params: Promise<{ id: string }> }) => {
5+
const { id } = await params;
6+
const initialData = await fetchMagnetPost(Number(id));
7+
8+
return <MagnetPostPage magnetId={id} initialData={initialData} />;
9+
};
10+
11+
export default Page;
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
'use client';
2+
3+
import TextFieldLimit from '@/domain/admin/blog/TextFieldLimit';
4+
import { useMagnetPostForm } from '@/domain/admin/blog/magnet/hooks/useMagnetPostForm';
5+
import MagnetProgramRecommendSection from '@/domain/admin/blog/magnet/section/MagnetProgramRecommendSection';
6+
import MagnetRecommendSection from '@/domain/admin/blog/magnet/section/MagnetRecommendSection';
7+
import { MAGNET_TYPE, MagnetPostDetail } from '@/domain/admin/blog/magnet/types';
8+
import Heading from '@/domain/admin/ui/heading/Heading';
9+
import Heading2 from '@/domain/admin/ui/heading/Heading2';
10+
import ImageUpload from '@/domain/admin/program/ui/form/ImageUpload';
11+
import { Button, Checkbox, FormControlLabel } from '@mui/material';
12+
import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker';
13+
import dynamic from 'next/dynamic';
14+
15+
const EditorApp = dynamic(
16+
() => import('@/domain/admin/lexical/EditorApp'),
17+
{ ssr: false },
18+
);
19+
20+
const MAX_META_DESCRIPTION_LENGTH = 100;
21+
22+
interface MagnetPostPageProps {
23+
magnetId: string;
24+
initialData: MagnetPostDetail;
25+
}
26+
27+
const MagnetPostPage = ({ magnetId, initialData }: MagnetPostPageProps) => {
28+
const {
29+
type,
30+
title,
31+
formState,
32+
displayDate,
33+
endDate,
34+
content,
35+
initialEditorStateBefore,
36+
initialEditorStateAfter,
37+
onChangeMetaDescription,
38+
onChangeThumbnailFile,
39+
onChangeHasCommonForm,
40+
onChangeProgramRecommend,
41+
onChangeMagnetRecommend,
42+
onChangeEditorBefore,
43+
onChangeEditorAfter,
44+
setDisplayDate,
45+
setEndDate,
46+
savePost,
47+
navigateToList,
48+
} = useMagnetPostForm({ magnetId, initialData });
49+
50+
return (
51+
<div className="mx-6 mb-40 mt-6">
52+
<header className="mb-4">
53+
<Heading>마그넷 글 관리</Heading>
54+
</header>
55+
<main className="max-w-screen-xl">
56+
<div className="flex flex-col gap-6">
57+
{/* 4.1 타입 */}
58+
<p className="text-lg font-medium">
59+
타입: &nbsp;{MAGNET_TYPE[type]}
60+
</p>
61+
62+
{/* 4.2 제목 */}
63+
<p className="text-lg font-medium">
64+
제목: &nbsp;{title}
65+
</p>
66+
67+
{/* 4.3 메타 디스크립션 */}
68+
<TextFieldLimit
69+
type="text"
70+
label="메타 디스크립션"
71+
placeholder="메타 디스크립션"
72+
name="metaDescription"
73+
value={formState.metaDescription}
74+
onChange={onChangeMetaDescription}
75+
multiline
76+
minRows={3}
77+
fullWidth
78+
maxLength={MAX_META_DESCRIPTION_LENGTH}
79+
/>
80+
81+
{/* 4.4 썸네일 */}
82+
<div className="w-72">
83+
<ImageUpload
84+
label="썸네일 등록"
85+
id="magnet-thumbnail"
86+
image={formState.thumbnail}
87+
onChange={onChangeThumbnailFile}
88+
/>
89+
</div>
90+
91+
{/* 4.5 프로그램 추천 + 4.6 마그넷 추천 */}
92+
<div className="flex gap-5">
93+
<MagnetProgramRecommendSection
94+
programRecommend={content.programRecommend}
95+
onChangeProgramRecommend={onChangeProgramRecommend}
96+
/>
97+
<MagnetRecommendSection
98+
magnetRecommend={content.magnetRecommend}
99+
onChangeMagnetRecommend={onChangeMagnetRecommend}
100+
/>
101+
</div>
102+
103+
{/* 4.7 노출 기간 */}
104+
<div className="border px-6 py-10">
105+
<Heading2 className="mb-4">노출 기간</Heading2>
106+
<div className="flex gap-4">
107+
<DateTimePicker
108+
label="시작 일자"
109+
value={displayDate}
110+
onChange={setDisplayDate}
111+
format="YYYY.MM.DD(dd) HH:mm"
112+
ampm={false}
113+
/>
114+
<DateTimePicker
115+
label="종료 일자"
116+
value={endDate}
117+
onChange={setEndDate}
118+
format="YYYY.MM.DD(dd) HH:mm"
119+
ampm={false}
120+
/>
121+
</div>
122+
</div>
123+
124+
{/* 4.8 공통 신청폼 추가 */}
125+
<FormControlLabel
126+
control={
127+
<Checkbox
128+
checked={formState.hasCommonForm}
129+
onChange={(e) => onChangeHasCommonForm(e.target.checked)}
130+
/>
131+
}
132+
label="공통 신청폼 추가"
133+
/>
134+
135+
{/* 4.9 콘텐츠 편집1 (신청 전 공개) */}
136+
<div>
137+
<Heading2 className="mb-2">콘텐츠 편집1(신청 전 공개)</Heading2>
138+
<EditorApp
139+
initialEditorStateJsonString={initialEditorStateBefore}
140+
onChange={onChangeEditorBefore}
141+
/>
142+
</div>
143+
144+
{/* 4.10 콘텐츠 편집2 (신청 후 공개) */}
145+
<div>
146+
<Heading2 className="mb-2">콘텐츠 편집2(신청 후 공개)</Heading2>
147+
<EditorApp
148+
initialEditorStateJsonString={initialEditorStateAfter}
149+
onChange={onChangeEditorAfter}
150+
/>
151+
</div>
152+
153+
{/* 4.11 액션 버튼 */}
154+
<div className="flex items-center justify-end gap-4">
155+
<Button variant="outlined" type="button" onClick={navigateToList}>
156+
취소 (리스트로 돌아가기)
157+
</Button>
158+
<Button
159+
variant="contained"
160+
color="primary"
161+
type="button"
162+
onClick={savePost}
163+
>
164+
등록하기
165+
</Button>
166+
</div>
167+
</div>
168+
</main>
169+
</div>
170+
);
171+
};
172+
173+
export default MagnetPostPage;
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import { uploadFile } from '@/api/file';
2+
import { saveMagnetPost } from '@/domain/admin/blog/magnet/mock';
3+
import {
4+
MagnetPostContent,
5+
MagnetPostDetail,
6+
MagnetProgramRecommendItem,
7+
} from '@/domain/admin/blog/magnet/types';
8+
import { useAdminSnackbar } from '@/hooks/useAdminSnackbar';
9+
import dayjs from '@/lib/dayjs';
10+
import { Dayjs } from 'dayjs';
11+
import { useRouter } from 'next/navigation';
12+
import { ChangeEvent, useMemo, useState } from 'react';
13+
14+
const RECOMMEND_SLOT_COUNT = 4;
15+
16+
function createEmptyContent(): MagnetPostContent {
17+
return {
18+
programRecommend: Array.from({ length: RECOMMEND_SLOT_COUNT }, () => ({
19+
id: null,
20+
})),
21+
magnetRecommend: Array.from({ length: RECOMMEND_SLOT_COUNT }, () => null),
22+
};
23+
}
24+
25+
function parseInitialContent(data: MagnetPostDetail): MagnetPostContent {
26+
if (!data.content || data.content === '') return createEmptyContent();
27+
try {
28+
return JSON.parse(data.content);
29+
} catch {
30+
return createEmptyContent();
31+
}
32+
}
33+
34+
interface FormState {
35+
metaDescription: string;
36+
thumbnail: string;
37+
hasCommonForm: boolean;
38+
}
39+
40+
function buildInitialFormState(data: MagnetPostDetail): FormState {
41+
return {
42+
metaDescription: data.metaDescription ?? '',
43+
thumbnail: data.thumbnail ?? '',
44+
hasCommonForm: data.hasCommonForm ?? false,
45+
};
46+
}
47+
48+
interface UseMagnetPostFormParams {
49+
magnetId: string;
50+
initialData: MagnetPostDetail;
51+
}
52+
53+
export const useMagnetPostForm = ({
54+
magnetId,
55+
initialData,
56+
}: UseMagnetPostFormParams) => {
57+
const router = useRouter();
58+
const { snackbar: setSnackbar } = useAdminSnackbar();
59+
60+
const initialContent = useMemo(
61+
() => parseInitialContent(initialData),
62+
[initialData],
63+
);
64+
const initialFormState = useMemo(
65+
() => buildInitialFormState(initialData),
66+
[initialData],
67+
);
68+
69+
const [formState, setFormState] = useState<FormState>(initialFormState);
70+
const [displayDate, setDisplayDate] = useState<Dayjs | null>(
71+
initialData.displayDate ? dayjs(initialData.displayDate) : null,
72+
);
73+
const [endDate, setEndDate] = useState<Dayjs | null>(
74+
initialData.endDate ? dayjs(initialData.endDate) : null,
75+
);
76+
const [content, setContent] = useState<MagnetPostContent>(initialContent);
77+
78+
const onChangeMetaDescription = (e: ChangeEvent<HTMLInputElement>) => {
79+
setFormState((prev) => ({ ...prev, metaDescription: e.target.value }));
80+
};
81+
82+
const onChangeThumbnailFile = async (
83+
e: ChangeEvent<HTMLInputElement>,
84+
) => {
85+
const file = e.target.files?.item(0);
86+
if (!file) {
87+
setSnackbar('파일이 없습니다.');
88+
return;
89+
}
90+
const url = await uploadFile({ file, type: 'BLOG' });
91+
setFormState((prev) => ({ ...prev, thumbnail: url }));
92+
};
93+
94+
const onChangeHasCommonForm = (checked: boolean) => {
95+
setFormState((prev) => ({ ...prev, hasCommonForm: checked }));
96+
};
97+
98+
const onChangeProgramRecommend = (items: MagnetProgramRecommendItem[]) => {
99+
setContent((prev) => ({ ...prev, programRecommend: items }));
100+
};
101+
102+
const onChangeMagnetRecommend = (items: (number | null)[]) => {
103+
setContent((prev) => ({ ...prev, magnetRecommend: items }));
104+
};
105+
106+
const onChangeEditorBefore = (jsonString: string) => {
107+
setContent((prev) => ({ ...prev, lexicalBefore: jsonString }));
108+
};
109+
110+
const onChangeEditorAfter = (jsonString: string) => {
111+
setContent((prev) => ({ ...prev, lexicalAfter: jsonString }));
112+
};
113+
114+
const savePost = async () => {
115+
await saveMagnetPost({
116+
magnetId: Number(magnetId),
117+
metaDescription: formState.metaDescription,
118+
thumbnail: formState.thumbnail,
119+
displayDate: displayDate?.format('YYYY-MM-DDTHH:mm') ?? null,
120+
endDate: endDate?.format('YYYY-MM-DDTHH:mm') ?? null,
121+
hasCommonForm: formState.hasCommonForm,
122+
content: JSON.stringify(content),
123+
isVisible: false,
124+
});
125+
setSnackbar('마그넷 글이 저장되었습니다.');
126+
};
127+
128+
const navigateToList = () => {
129+
router.push('/admin/blog/magnet/list');
130+
};
131+
132+
return {
133+
type: initialData.type,
134+
title: initialData.title,
135+
formState,
136+
displayDate,
137+
endDate,
138+
content,
139+
initialEditorStateBefore: initialContent.lexicalBefore,
140+
initialEditorStateAfter: initialContent.lexicalAfter,
141+
onChangeMetaDescription,
142+
onChangeThumbnailFile,
143+
onChangeHasCommonForm,
144+
onChangeProgramRecommend,
145+
onChangeMagnetRecommend,
146+
onChangeEditorBefore,
147+
onChangeEditorAfter,
148+
setDisplayDate,
149+
setEndDate,
150+
savePost,
151+
navigateToList,
152+
};
153+
};

0 commit comments

Comments
 (0)