-
Notifications
You must be signed in to change notification settings - Fork 2
LC-2880 ADMIN 마그넷 내부화 글 등록 API 연동 #2150
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: LC-2838-Sprint-17
Are you sure you want to change the base?
The head ref may contain hidden characters: "LC-2880-ADMIN-\uB9C8\uADF8\uB137-\uB0B4\uBD80\uD654-\uAE00-\uB4F1\uB85D-API-\uC5F0\uB3D9"
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,9 @@ | ||
| 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} />; | ||
| return <MagnetPostPage magnetId={id} />; | ||
| }; | ||
|
|
||
| export default Page; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,10 @@ | ||
| import { uploadFile } from '@/api/file'; | ||
| import { saveMagnetPost } from '@/domain/admin/blog/magnet/mock'; | ||
| import { | ||
| useGetMagnetDetailQuery, | ||
| usePatchMagnetMutation, | ||
| } from '@/api/magnet/magnet'; | ||
| import { | ||
| MagnetPostContent, | ||
| MagnetPostDetail, | ||
| MagnetProgramRecommendItem, | ||
| } from '@/domain/admin/blog/magnet/types'; | ||
| import { useAdminSnackbar } from '@/hooks/useAdminSnackbar'; | ||
|
|
@@ -22,12 +24,35 @@ function createEmptyContent(): MagnetPostContent { | |
| }; | ||
| } | ||
|
|
||
| function parseInitialContent(data: MagnetPostDetail): MagnetPostContent { | ||
| if (!data.content || data.content === '') return createEmptyContent(); | ||
| /** description JSON에서 metaDescription, programRecommend, magnetRecommend 추출 */ | ||
| interface DescriptionPayload { | ||
| metaDescription: string; | ||
| programRecommend: MagnetProgramRecommendItem[]; | ||
| magnetRecommend: (number | null)[]; | ||
| } | ||
|
|
||
| function parseDescription(raw: string | null): DescriptionPayload { | ||
| const empty = createEmptyContent(); | ||
| if (!raw) { | ||
| return { | ||
| metaDescription: '', | ||
| programRecommend: empty.programRecommend, | ||
| magnetRecommend: empty.magnetRecommend, | ||
| }; | ||
| } | ||
| try { | ||
| return JSON.parse(data.content); | ||
| const parsed = JSON.parse(raw); | ||
| return { | ||
| metaDescription: parsed.metaDescription ?? '', | ||
| programRecommend: parsed.programRecommend ?? empty.programRecommend, | ||
| magnetRecommend: parsed.magnetRecommend ?? empty.magnetRecommend, | ||
| }; | ||
| } catch { | ||
| return createEmptyContent(); | ||
| return { | ||
| metaDescription: raw, | ||
| programRecommend: empty.programRecommend, | ||
| magnetRecommend: empty.magnetRecommend, | ||
| }; | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -37,43 +62,70 @@ interface FormState { | |
| 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) => { | ||
| export const useMagnetPostForm = (magnetId: number) => { | ||
| const router = useRouter(); | ||
| const { snackbar: setSnackbar } = useAdminSnackbar(); | ||
|
|
||
| const initialContent = useMemo( | ||
| () => parseInitialContent(initialData), | ||
| [initialData], | ||
| ); | ||
| const initialFormState = useMemo( | ||
| () => buildInitialFormState(initialData), | ||
| [initialData], | ||
| const { data: detailData, isLoading } = useGetMagnetDetailQuery(magnetId); | ||
| const { mutate: patchMagnet } = usePatchMagnetMutation({ | ||
| successCallback: () => { | ||
| setSnackbar('마그넷 글이 저장되었습니다.'); | ||
| }, | ||
| }); | ||
|
|
||
| const magnetInfo = detailData?.magnetInfo; | ||
|
|
||
| const descPayload = useMemo( | ||
| () => parseDescription(magnetInfo?.description ?? null), | ||
| [magnetInfo?.description], | ||
| ); | ||
|
|
||
| const [formState, setFormState] = useState<FormState>(initialFormState); | ||
| const [displayDate, setDisplayDate] = useState<Dayjs | null>( | ||
| initialData.displayDate ? dayjs(initialData.displayDate) : null, | ||
| const initialContent = useMemo<MagnetPostContent>( | ||
| () => ({ | ||
| programRecommend: descPayload.programRecommend, | ||
| magnetRecommend: descPayload.magnetRecommend, | ||
| lexicalBefore: magnetInfo?.previewContents || undefined, | ||
| lexicalAfter: magnetInfo?.mainContents || undefined, | ||
| }), | ||
| [descPayload, magnetInfo?.previewContents, magnetInfo?.mainContents], | ||
| ); | ||
| const [endDate, setEndDate] = useState<Dayjs | null>( | ||
| initialData.endDate ? dayjs(initialData.endDate) : null, | ||
|
|
||
| const [formState, setFormState] = useState<FormState>({ | ||
| metaDescription: '', | ||
| thumbnail: '', | ||
| hasCommonForm: false, | ||
| }); | ||
| const [formInitialized, setFormInitialized] = useState(false); | ||
|
|
||
| // detailData가 로드되면 폼 상태 초기화 | ||
| if (magnetInfo && !formInitialized) { | ||
| setFormState({ | ||
| metaDescription: descPayload.metaDescription, | ||
| thumbnail: magnetInfo.desktopThumbnail ?? '', | ||
| hasCommonForm: false, | ||
| }); | ||
| setFormInitialized(true); | ||
| } | ||
|
|
||
| const [displayDate, setDisplayDate] = useState<Dayjs | null>(null); | ||
| const [endDate, setEndDate] = useState<Dayjs | null>(null); | ||
| const [dateInitialized, setDateInitialized] = useState(false); | ||
|
|
||
| if (magnetInfo && !dateInitialized) { | ||
| setDisplayDate(magnetInfo.startDate ? dayjs(magnetInfo.startDate) : null); | ||
| setEndDate(magnetInfo.endDate ? dayjs(magnetInfo.endDate) : null); | ||
| setDateInitialized(true); | ||
| } | ||
|
|
||
| const [content, setContent] = useState<MagnetPostContent>( | ||
| createEmptyContent(), | ||
| ); | ||
| const [content, setContent] = useState<MagnetPostContent>(initialContent); | ||
| const [contentInitialized, setContentInitialized] = useState(false); | ||
|
|
||
| if (magnetInfo && !contentInitialized) { | ||
| setContent(initialContent); | ||
| setContentInitialized(true); | ||
| } | ||
|
Comment on lines
+93
to
+128
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 데이터 로딩 후 폼 상태를 초기화하기 위해 렌더링 중에 이러한 상태 초기화 로직은 아래와 같이 하나의 const [formState, setFormState] = useState<FormState>({
metaDescription: '',
thumbnail: '',
hasCommonForm: false,
});
const [displayDate, setDisplayDate] = useState<Dayjs | null>(null);
const [endDate, setEndDate] = useState<Dayjs | null>(null);
const [content, setContent] = useState<MagnetPostContent>(
createEmptyContent(),
);
useEffect(() => {
if (magnetInfo) {
setFormState({
metaDescription: descPayload.metaDescription,
thumbnail: magnetInfo.desktopThumbnail ?? '',
hasCommonForm: false,
});
setDisplayDate(magnetInfo.startDate ? dayjs(magnetInfo.startDate) : null);
setEndDate(magnetInfo.endDate ? dayjs(magnetInfo.endDate) : null);
setContent(initialContent);
}
}, [magnetInfo, descPayload, initialContent]);References
|
||
|
|
||
| const onChangeMetaDescription = (e: ChangeEvent<HTMLInputElement>) => { | ||
| setFormState((prev) => ({ ...prev, metaDescription: e.target.value })); | ||
|
|
@@ -111,27 +163,33 @@ export const useMagnetPostForm = ({ | |
| setContent((prev) => ({ ...prev, lexicalAfter: jsonString })); | ||
| }; | ||
|
|
||
| const savePost = async () => { | ||
| await saveMagnetPost({ | ||
| magnetId: Number(magnetId), | ||
| const savePost = () => { | ||
| const description = JSON.stringify({ | ||
| metaDescription: formState.metaDescription, | ||
| thumbnail: formState.thumbnail, | ||
| displayDate: displayDate?.format('YYYY-MM-DDTHH:mm') ?? null, | ||
| programRecommend: content.programRecommend, | ||
| magnetRecommend: content.magnetRecommend, | ||
| }); | ||
|
|
||
| patchMagnet({ | ||
| magnetId, | ||
| description, | ||
| previewContents: content.lexicalBefore ?? '', | ||
| mainContents: content.lexicalAfter ?? '', | ||
| desktopThumbnail: formState.thumbnail, | ||
| mobileThumbnail: formState.thumbnail, | ||
| startDate: 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, | ||
| isLoading, | ||
| type: magnetInfo?.type, | ||
| title: magnetInfo?.title, | ||
| formState, | ||
| displayDate, | ||
| endDate, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
zod 스키마에서
questionType,isRequired,selectionMethod필드를z.string()으로 정의하셨습니다. 하지만src/domain/admin/blog/magnet/types.ts에 정의된 관련 타입들을 보면 이 필드들은 특정 문자열 값만 허용하는 enum 형태입니다.questionType:'SUBJECTIVE' | 'OBJECTIVE'isRequired:'REQUIRED' | 'OPTIONAL'selectionMethod:'SINGLE' | 'MULTIPLE'z.string()대신z.enum()을 사용하면 API 응답을 더 엄격하게 검증하고 타입 안정성을 높일 수 있습니다. 예상치 못한 값이 들어왔을 때 런타임 에러를 방지하는 데 도움이 됩니다.