diff --git a/actions/about.ts b/actions/about.ts index 7bebbd3c5..cfdf1a4a6 100644 --- a/actions/about.ts +++ b/actions/about.ts @@ -9,13 +9,12 @@ import { deleteFacility, putFacility } from '@/apis/v2/about/facilities/[id]'; import { putFutureCareers } from '@/apis/v2/about/future-careers'; import { postCareerCompany } from '@/apis/v2/about/future-careers/company'; import { deleteCareerCompany, putCareerCompany } from '@/apis/v2/about/future-careers/company/[id]'; -import { postCareerStat, putCareerStat } from '@/apis/v2/about/future-careers/stats'; +import { CareerStat, postCareerStat, putCareerStat } from '@/apis/v2/about/future-careers/stats'; import { putGreetings } from '@/apis/v2/about/greetings'; import { putHistory } from '@/apis/v2/about/history'; import { putOverview } from '@/apis/v2/about/overview'; import { postClub, putClub } from '@/apis/v2/about/student-clubs'; import { deleteClub } from '@/apis/v2/about/student-clubs/[id]'; -import { CareerStatEditorContent } from '@/components/editor/CareerStatEditor'; import { FETCH_TAG_CAREER, FETCH_TAG_CLUB, @@ -84,13 +83,13 @@ export const putCareerDescriptionAction = withErrorHandler( }, ); -export const postCareerStatAction = withErrorHandler(async (data: CareerStatEditorContent) => { +export const postCareerStatAction = withErrorHandler(async (data: CareerStat) => { await postCareerStat(data); revalidateTag(FETCH_TAG_CAREER); redirectKo(careerPath); }); -export const putCareerStatAction = withErrorHandler(async (data: CareerStatEditorContent) => { +export const putCareerStatAction = withErrorHandler(async (data: CareerStat) => { await putCareerStat(data); revalidateTag(FETCH_TAG_CAREER); redirectKo(careerPath); diff --git a/actions/academics.ts b/actions/academics.ts index b1722cbe0..b878cdde1 100644 --- a/actions/academics.ts +++ b/actions/academics.ts @@ -2,13 +2,8 @@ import { revalidateTag } from 'next/cache'; -import { postCourseChanges } from '@/apis/v1/academics/[type]/course-changes'; -import { - deleteCourseChanges, - putCourseChanges, -} from '@/apis/v1/academics/[type]/course-changes/[year]'; -import { putAcademicsGuide } from '@/apis/v1/academics/[type]/guide'; -import { postCurriculum } from '@/apis/v1/academics/undergraduate/curriculum'; +import { deleteCourseChanges } from '@/apis/v1/academics/[studentType]/course-changes/[year]'; +import { putAcademicsGuide } from '@/apis/v1/academics/[studentType]/guide'; import { deleteCurriculum, putCurriculum, @@ -36,7 +31,6 @@ import { import { redirect } from '@/i18n/routing'; import { Course, - CourseChange, Curriculum, GeneralStudiesRequirement, Scholarship, @@ -74,11 +68,6 @@ export const deleteCourseAction = withErrorHandler(async (code: string) => { /** 전공 이수 표준 형태 */ -export const postCurriculumAction = withErrorHandler(async (data: Curriculum) => { - await postCurriculum(data); - revalidateTag(FETCH_TAG_CURRICULUM); -}); - export const putCurriculumAction = withErrorHandler(async (data: Curriculum) => { await putCurriculum(data); revalidateTag(FETCH_TAG_CURRICULUM); @@ -118,20 +107,6 @@ export const putDegreeRequirementsAction = withErrorHandler(async (formData: For /** 교과목 변경 내역 */ -export const postCourseChangesAction = withErrorHandler( - async (type: StudentType, data: CourseChange) => { - await postCourseChanges(type, data); - revalidateTag(FETCH_TAG_COURSE_CHANGES); - }, -); - -export const putCourseChangesAction = withErrorHandler( - async (type: StudentType, data: CourseChange) => { - await putCourseChanges(type, data); - revalidateTag(FETCH_TAG_COURSE_CHANGES); - }, -); - export const deleteCourseChangesAction = withErrorHandler( async (type: StudentType, year: number) => { await deleteCourseChanges(type, year); diff --git a/actions/internal.ts b/actions/internal.ts deleted file mode 100644 index 070a88cb8..000000000 --- a/actions/internal.ts +++ /dev/null @@ -1,13 +0,0 @@ -'use server'; - -import { revalidateTag } from 'next/cache'; - -import { putInternal } from '@/apis/v1/internal'; -import { FETCH_TAG_INTERNAL } from '@/constants/network'; - -import { withErrorHandler } from './errorHandler'; - -export const putInternalAction = withErrorHandler(async (description: string) => { - await putInternal(description); - revalidateTag(FETCH_TAG_INTERNAL); -}); diff --git a/apis/index.ts b/apis/index.ts index dfeced20a..58d30050a 100644 --- a/apis/index.ts +++ b/apis/index.ts @@ -1,11 +1,10 @@ import { cookies } from 'next/headers'; +import { BASE_URL } from '@/constants/network'; import { objToQueryString } from '@/utils/convertParams'; type CredentialRequestInit = RequestInit & { jsessionID?: boolean }; -export const BASE_URL = process.env.BASE_URL; - export const getRequest = async ( url: string, params: object = {}, diff --git a/apis/v1/academics/[studentType]/[postType].ts b/apis/v1/academics/[studentType]/[postType].ts new file mode 100644 index 000000000..677b33ba7 --- /dev/null +++ b/apis/v1/academics/[studentType]/[postType].ts @@ -0,0 +1,28 @@ +'use server'; + +import { getRequest, postRequest, putRequest } from '@/apis'; +import { Attachment } from '@/components/common/Attachments'; +import { StudentType } from '@/types/academics'; + +export interface AcademicsByPostType { + year: number; + description: string; + attachments: Attachment[]; +} + +export type PostType = 'course-changes' | 'curriculum' | 'general-studies-requirements'; + +export const getAcademicsByPostType = (studentType: StudentType, postType: PostType) => + getRequest(`/v1/academics/${studentType}/${postType}`); + +export const postAcademicsByPostType = ( + studentType: StudentType, + postType: PostType, + body: FormData, +) => postRequest(`/v1/academics/${studentType}/${postType}`, { body, jsessionID: true }); + +export const putAcademicsByPostType = ( + studentType: StudentType, + postType: PostType, + body: FormData, +) => putRequest(`/v1/academics/${studentType}/${postType}`, { body, jsessionID: true }); diff --git a/apis/v1/academics/[studentType]/course-changes/[year].ts b/apis/v1/academics/[studentType]/course-changes/[year].ts new file mode 100644 index 000000000..40b3ccd17 --- /dev/null +++ b/apis/v1/academics/[studentType]/course-changes/[year].ts @@ -0,0 +1,10 @@ +import { deleteRequest, putRequest } from '@/apis'; +import { StudentType } from '@/types/academics'; + +export const putCourseChanges = (type: StudentType, year: number, body: FormData) => + putRequest(`/v1/academics/${type}/course-changes/${year}`, { body, jsessionID: true }); + +export const deleteCourseChanges = async (type: StudentType, year: number) => + deleteRequest(`/v1/academics/${type}/course-changes/${year}`, { + jsessionID: true, + }); diff --git a/apis/v1/academics/[type]/course-changes/index.ts b/apis/v1/academics/[studentType]/course-changes/index.ts similarity index 61% rename from apis/v1/academics/[type]/course-changes/index.ts rename to apis/v1/academics/[studentType]/course-changes/index.ts index 8b824dcb3..2cc80307f 100644 --- a/apis/v1/academics/[type]/course-changes/index.ts +++ b/apis/v1/academics/[studentType]/course-changes/index.ts @@ -1,13 +1,14 @@ import { getRequest, postRequest } from '@/apis'; +import { AcademicsCommon } from '@/apis/v1/academics/types'; import { FETCH_TAG_COURSE_CHANGES } from '@/constants/network'; -import { CourseChange, StudentType } from '@/types/academics'; +import { StudentType } from '@/types/academics'; export const getCourseChanges = (type: StudentType) => - getRequest(`/v1/academics/${type}/course-changes`, undefined, { + getRequest(`/v1/academics/${type}/course-changes`, undefined, { next: { tags: [FETCH_TAG_COURSE_CHANGES] }, }); -export const postCourseChanges = (type: StudentType, data: CourseChange) => +export const postCourseChanges = (type: StudentType, data: AcademicsCommon) => postRequest(`/v1/academics/${type}/course-changes`, { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...data, name: '교과목 변경 내역' }), diff --git a/apis/v1/academics/[type]/guide.ts b/apis/v1/academics/[studentType]/guide.ts similarity index 100% rename from apis/v1/academics/[type]/guide.ts rename to apis/v1/academics/[studentType]/guide.ts diff --git a/apis/v1/academics/[type]/course-changes/[year].ts b/apis/v1/academics/[type]/course-changes/[year].ts deleted file mode 100644 index 57c337c4c..000000000 --- a/apis/v1/academics/[type]/course-changes/[year].ts +++ /dev/null @@ -1,14 +0,0 @@ -import { deleteRequest, putRequest } from '@/apis'; -import { CourseChange, StudentType } from '@/types/academics'; - -export const putCourseChanges = (type: StudentType, data: CourseChange) => - putRequest(`/v1/academics/${type}/course-changes/${data.year}`, { - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ description: data.description }), - jsessionID: true, - }); - -export const deleteCourseChanges = async (type: StudentType, year: number) => - deleteRequest(`/v1/academics/${type}/course-changes/${year}`, { - jsessionID: true, - }); diff --git a/apis/v1/academics/types.ts b/apis/v1/academics/types.ts new file mode 100644 index 000000000..b17e01c7d --- /dev/null +++ b/apis/v1/academics/types.ts @@ -0,0 +1,7 @@ +import { Attachment } from '@/components/common/Attachments'; + +export interface AcademicsCommon { + year: number; + description: string; + attachments: Attachment[]; +} diff --git a/apis/v2/about/future-careers/stats.ts b/apis/v2/about/future-careers/stats.ts index e876b0489..db8ac8241 100644 --- a/apis/v2/about/future-careers/stats.ts +++ b/apis/v2/about/future-careers/stats.ts @@ -1,14 +1,19 @@ import { postRequest, putRequest } from '@/apis'; -import { CareerStatEditorContent } from '@/components/editor/CareerStatEditor'; +import { Stat } from './types'; -export const postCareerStat = (data: CareerStatEditorContent) => +export interface CareerStat { + year: number; + statList: Stat[]; +} + +export const postCareerStat = (data: CareerStat) => postRequest('/v2/about/future-careers/stats', { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), jsessionID: true, }); -export const putCareerStat = (data: CareerStatEditorContent) => +export const putCareerStat = (data: CareerStat) => putRequest('/v2/about/future-careers/stats', { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), diff --git a/apis/v2/about/future-careers/types.ts b/apis/v2/about/future-careers/types.ts new file mode 100644 index 000000000..80107fe38 --- /dev/null +++ b/apis/v2/about/future-careers/types.ts @@ -0,0 +1,15 @@ +export const companyList = ['SAMSUNG', 'LG', 'LARGE', 'SMALL', 'GRADUATE', 'OTHER'] as const; +export type Company = (typeof companyList)[number]; +export const degreeList = ['bachelor', 'master', 'doctor'] as const; +export type Degree = (typeof degreeList)[number]; + +export type Stat = { career: Company } & { [key in Degree]: number }; + +export const COMPANY_MAP = { + SAMSUNG: '삼성', + LG: 'LG', + LARGE: '기타 대기업', + SMALL: '중소기업', + GRADUATE: '진학', + OTHER: '기타', +} as const; diff --git a/app/.internal/InternalContent.tsx b/app/.internal/InternalContent.tsx deleted file mode 100644 index e9682a54a..000000000 --- a/app/.internal/InternalContent.tsx +++ /dev/null @@ -1,44 +0,0 @@ -'use client'; - -import { useReducer } from 'react'; - -import { putInternalAction } from '@/actions/internal'; -import { GrayButton } from '@/components/common/Buttons'; -import LoginVisible from '@/components/common/LoginVisible'; -import BasicEditor, { BasicEditorContent } from '@/components/editor/BasicEditor'; -import HTMLViewer from '@/components/editor/HTMLViewer'; -import { errorToStr } from '@/utils/error'; -import { handleServerAction } from '@/utils/serverActionError'; -import { errorToast, successToast } from '@/utils/toast'; - -export default function InternalContent({ description }: { description: string }) { - const [isEditMode, toggleEditMode] = useReducer((x) => !x, false); - - const handleSubmit = async (content: BasicEditorContent) => { - try { - handleServerAction(await putInternalAction(content.description.ko)); - successToast('본문을 수정했습니다.'); - toggleEditMode(); - } catch (e) { - errorToast(errorToStr(e)); - } - }; - - return isEditMode ? ( - - ) : ( - <> - - - - - - ); -} diff --git a/app/.internal/components/EditButton.tsx b/app/.internal/components/EditButton.tsx new file mode 100644 index 000000000..18a9ff80c --- /dev/null +++ b/app/.internal/components/EditButton.tsx @@ -0,0 +1,9 @@ +'use client'; + +import { GrayButton } from '@/components/common/Buttons'; +import { useRouter } from '@/i18n/routing'; + +export default function EditButton() { + const router = useRouter(); + return router.push('/.internal/edit')} />; +} diff --git a/app/.internal/components/InternalEditor.tsx b/app/.internal/components/InternalEditor.tsx new file mode 100644 index 000000000..5ca1f9d92 --- /dev/null +++ b/app/.internal/components/InternalEditor.tsx @@ -0,0 +1,49 @@ +'use client'; + +import { FormProvider, useForm } from 'react-hook-form'; + +import Fieldset from '@/components/form/Fieldset'; +import Form from '@/components/form/Form'; +import { useRouter } from '@/i18n/routing'; +import { errorToStr } from '@/utils/error'; +import { handleServerAction } from '@/utils/serverActionError'; +import { errorToast } from '@/utils/toast'; + +interface FormData { + description: string; +} + +export default function InternalEditor({ + description, + onSubmit: _onSubmit, +}: { + description: string; + onSubmit: (description: string) => Promise; +}) { + const formMethods = useForm({ defaultValues: { description } }); + const { handleSubmit } = formMethods; + + const router = useRouter(); + const onCancel = () => router.push('.internal'); + + const onSubmit = async ({ description }: FormData) => { + try { + handleServerAction(await _onSubmit(description)); + } catch (e) { + errorToast(errorToStr(e)); + } + }; + + return ( + +
+
+ + + + + +
+
+ ); +} diff --git a/app/.internal/edit/page.tsx b/app/.internal/edit/page.tsx new file mode 100644 index 000000000..ff63913c9 --- /dev/null +++ b/app/.internal/edit/page.tsx @@ -0,0 +1,19 @@ +import { revalidateTag } from 'next/cache'; + +import { getInternal, putInternal } from '@/apis/v1/internal'; +import InternalEditor from '@/app/.internal/components/InternalEditor'; +import { FETCH_TAG_INTERNAL } from '@/constants/network'; +import { redirectKo } from '@/i18n/routing'; + +export default async function InternalPage() { + const { description } = await getInternal(); + + const onSubmit = async (description: string) => { + 'use server'; + await putInternal(description); + revalidateTag(FETCH_TAG_INTERNAL); + redirectKo('/.internal'); + }; + + return ; +} diff --git a/app/.internal/page.tsx b/app/.internal/page.tsx index 3afd33d39..4a6c7929b 100644 --- a/app/.internal/page.tsx +++ b/app/.internal/page.tsx @@ -1,13 +1,17 @@ import { getInternal } from '@/apis/v1/internal'; - -import InternalContent from './InternalContent'; +import EditButton from '@/app/.internal/components/EditButton'; +import LoginVisible from '@/components/common/LoginVisible'; +import HTMLViewer from '@/components/form/html/HTMLViewer'; export default async function InternalPage() { const { description } = await getInternal(); return (
- + + + +
); } diff --git a/app/[locale]/10-10-project/manager/page.tsx b/app/[locale]/10-10-project/manager/page.tsx index c84297a53..831309e6f 100644 --- a/app/[locale]/10-10-project/manager/page.tsx +++ b/app/[locale]/10-10-project/manager/page.tsx @@ -1,4 +1,4 @@ -import HTMLViewer from '@/components/editor/HTMLViewer'; +import HTMLViewer from '@/components/form/html/HTMLViewer'; import PageLayout from '@/components/layout/pageLayout/PageLayout'; import { getPath } from '@/utils/page'; import { greetings } from '@/utils/segmentNode'; diff --git a/app/[locale]/10-10-project/participants/page.tsx b/app/[locale]/10-10-project/participants/page.tsx index 6c7751125..ea197de14 100644 --- a/app/[locale]/10-10-project/participants/page.tsx +++ b/app/[locale]/10-10-project/participants/page.tsx @@ -1,4 +1,4 @@ -import HTMLViewer from '@/components/editor/HTMLViewer'; +import HTMLViewer from '@/components/form/html/HTMLViewer'; import PageLayout from '@/components/layout/pageLayout/PageLayout'; export default async function TenTenParticipants() { diff --git a/app/[locale]/10-10-project/proposal/page.tsx b/app/[locale]/10-10-project/proposal/page.tsx index 15eac7613..d96efa1e0 100644 --- a/app/[locale]/10-10-project/proposal/page.tsx +++ b/app/[locale]/10-10-project/proposal/page.tsx @@ -1,4 +1,4 @@ -import HTMLViewer from '@/components/editor/HTMLViewer'; +import HTMLViewer from '@/components/form/html/HTMLViewer'; import PageLayout from '@/components/layout/pageLayout/PageLayout'; export default async function TenTenProposal() { diff --git a/app/[locale]/about/AboutEditPageContent.tsx b/app/[locale]/about/AboutEditPageContent.tsx deleted file mode 100644 index e0cebf7b1..000000000 --- a/app/[locale]/about/AboutEditPageContent.tsx +++ /dev/null @@ -1,103 +0,0 @@ -'use client'; - -import { - putContactAction, - putGreetingsAction, - putHistoryAction, - putOverviewAction, -} from '@/actions/about'; -import { Attachment } from '@/components/common/Attachments'; -import BasicEditor, { BasicEditorContent } from '@/components/editor/BasicEditor'; -import PageLayout from '@/components/layout/pageLayout/PageLayout'; -import { useRouter } from '@/i18n/routing'; -import { AboutContent } from '@/types/about'; -import { WithLanguage } from '@/types/language'; -import { errorToStr } from '@/utils/error'; -import { contentToFormData, getAttachmentDeleteIds } from '@/utils/formData'; -import { validateBasicForm } from '@/utils/formValidation'; -import { getPath } from '@/utils/page'; -import { contact, greetings, history, overview, SegmentNode } from '@/utils/segmentNode'; -import { handleServerAction } from '@/utils/serverActionError'; -import { errorToast, successToast } from '@/utils/toast'; - -interface AboutEditPageContentProps { - data: WithLanguage; - node: SegmentNode; - showAttachments?: boolean; -} - -export default function AboutEditPageContent({ - data, - node, - showAttachments = false, -}: AboutEditPageContentProps) { - const router = useRouter(); - - const handleCancel = () => router.push(getPath(node)); - - const handleSubmit = async (content: BasicEditorContent) => { - validateBasicForm(content); - - const formData = contentToFormData('EDIT', { - requestObject: getRequestObject( - content, - data.ko.imageURL !== null && content.mainImage === null, - { ko: data.ko.attachments, en: data.en.attachments }, - ), - image: content.mainImage, - attachments: content.attachments, - }); - - try { - const submitAction = ABOUT_SUBMIT_ACTION[node.segment]; - handleServerAction(await submitAction(formData)); - successToast(`${node.name}을(를) 수정했습니다.`); - } catch (e) { - errorToast(errorToStr(e)); - } - }; - - return ( - - ({ type: 'UPLOADED_FILE', file })), - }} - actions={{ type: 'EDIT', onCancel: handleCancel, onSubmit: handleSubmit }} - showLanguage - showMainImage - showAttachments={showAttachments} - /> - - ); -} - -const getRequestObject = ( - newContent: BasicEditorContent, - removeImage: boolean, - prevAttachments: WithLanguage, -) => { - const koDeleteIds = getAttachmentDeleteIds( - newContent.attachments, - prevAttachments.ko.map((x) => x.id), - ); - const enDeleteIds = getAttachmentDeleteIds( - newContent.attachments, - prevAttachments.en.map((x) => x.id), - ); - - return { - ko: { description: newContent.description.ko, deleteIds: koDeleteIds }, - en: { description: newContent.description.en, deleteIds: enDeleteIds }, - removeImage, - }; -}; - -const ABOUT_SUBMIT_ACTION = { - [overview.segment]: putOverviewAction, - [greetings.segment]: putGreetingsAction, - [history.segment]: putHistoryAction, - [contact.segment]: putContactAction, -}; diff --git a/app/[locale]/about/components/AboutEditor.tsx b/app/[locale]/about/components/AboutEditor.tsx new file mode 100644 index 000000000..3059071bb --- /dev/null +++ b/app/[locale]/about/components/AboutEditor.tsx @@ -0,0 +1,117 @@ +'use client'; + +import { useState } from 'react'; +import { FormProvider, useForm } from 'react-hook-form'; + +import { + putContactAction, + putGreetingsAction, + putHistoryAction, + putOverviewAction, +} from '@/actions/about'; +import Fieldset from '@/components/form/Fieldset'; +import Form from '@/components/form/Form'; +import LanguagePicker from '@/components/form/LanguagePicker'; +import { PostEditorFile, PostEditorImage } from '@/components/form/types'; +import PageLayout from '@/components/layout/pageLayout/PageLayout'; +import { useRouter } from '@/i18n/routing'; +import { AboutContent } from '@/types/about'; +import { Language, WithLanguage } from '@/types/language'; +import { errorToStr } from '@/utils/error'; +import { contentToFormData, getAttachmentDeleteIds } from '@/utils/formData'; +import { getPath } from '@/utils/page'; +import { contact, greetings, history, overview, SegmentNode } from '@/utils/segmentNode'; +import { handleServerAction } from '@/utils/serverActionError'; +import { errorToast, successToast } from '@/utils/toast'; + +interface Props { + data: WithLanguage; + node: SegmentNode; + showAttachments?: boolean; +} + +interface FormData { + htmlKo: string; + htmlEn: string; + image: PostEditorImage; + files: PostEditorFile[]; +} + +export default function AboutEditor({ data, node, showAttachments = false }: Props) { + const router = useRouter(); + const formMethods = useForm({ + defaultValues: { + htmlKo: data.ko.description, + htmlEn: data.en.description, + image: data.ko.imageURL ? { type: 'UPLOADED_IMAGE', url: data.ko.imageURL } : null, + files: data.ko.attachments.map((attachment) => ({ file: attachment, type: 'UPLOADED_FILE' })), + }, + }); + const { handleSubmit } = formMethods; + + const [language, setLanguage] = useState('ko'); + + const onCancel = () => router.push(getPath(node)); + + const onSubmit = handleSubmit(async ({ htmlKo, htmlEn, image, files }) => { + try { + const submitAction = ABOUT_SUBMIT_ACTION[node.segment]; + + const requestObject = { + ko: { + description: htmlKo, + deleteIds: getAttachmentDeleteIds(files, data.ko.attachments), + }, + en: { + description: htmlEn, + deleteIds: getAttachmentDeleteIds(files, data.en.attachments), + }, + removeImage: data.ko.imageURL !== null && image === null, + }; + + const formData = contentToFormData('EDIT', { requestObject, image, attachments: files }); + + handleServerAction(submitAction(formData)); + successToast(`${node.name}을(를) 수정했습니다.`); + } catch (e) { + errorToast(errorToStr(e)); + } + }); + + return ( + + +
+ + + + {language === 'ko' && } + {language === 'en' && } + + + + + + + + {showAttachments && ( + + + + )} + + + +
+
+ ); +} + +const ABOUT_SUBMIT_ACTION = { + [overview.segment]: putOverviewAction, + [greetings.segment]: putGreetingsAction, + [history.segment]: putHistoryAction, + [contact.segment]: putContactAction, +}; diff --git a/app/[locale]/about/contact/edit/page.tsx b/app/[locale]/about/contact/edit/page.tsx index 468d1d9b0..a02f1bb6f 100644 --- a/app/[locale]/about/contact/edit/page.tsx +++ b/app/[locale]/about/contact/edit/page.tsx @@ -1,10 +1,10 @@ import { getContact } from '@/apis/v1/about/contact'; import { contact } from '@/utils/segmentNode'; -import AboutEditPageContent from '../../AboutEditPageContent'; +import AboutEditor from '../../components/AboutEditor'; export default async function ContactEditPage() { const [koData, enData] = await Promise.all([getContact('ko'), getContact('en')]); - return ; + return ; } diff --git a/app/[locale]/about/contact/page.tsx b/app/[locale]/about/contact/page.tsx index 14e010e99..9a09ab185 100644 --- a/app/[locale]/about/contact/page.tsx +++ b/app/[locale]/about/contact/page.tsx @@ -3,7 +3,7 @@ export const dynamic = 'force-dynamic'; import { getContact } from '@/apis/v1/about/contact'; import { EditButton } from '@/components/common/Buttons'; import LoginVisible from '@/components/common/LoginVisible'; -import HTMLViewer from '@/components/editor/HTMLViewer'; +import HTMLViewer from '@/components/form/html/HTMLViewer'; import PageLayout from '@/components/layout/pageLayout/PageLayout'; import { Language } from '@/types/language'; import { getMetadata } from '@/utils/metadata'; diff --git a/app/[locale]/about/directions/DirectionsDetails.tsx b/app/[locale]/about/directions/DirectionsDetails.tsx index ee50f05bc..04645c71e 100644 --- a/app/[locale]/about/directions/DirectionsDetails.tsx +++ b/app/[locale]/about/directions/DirectionsDetails.tsx @@ -1,6 +1,6 @@ import { EditButton } from '@/components/common/Buttons'; import LoginVisible from '@/components/common/LoginVisible'; -import HTMLViewer from '@/components/editor/HTMLViewer'; +import HTMLViewer from '@/components/form/html/HTMLViewer'; import { Direction } from '@/types/about'; import { getPath } from '@/utils/page'; import { directions } from '@/utils/segmentNode'; diff --git a/app/[locale]/about/directions/edit/DirectionEditor.tsx b/app/[locale]/about/directions/edit/DirectionEditor.tsx new file mode 100644 index 000000000..14cbef355 --- /dev/null +++ b/app/[locale]/about/directions/edit/DirectionEditor.tsx @@ -0,0 +1,68 @@ +'use client'; + +import { useState } from 'react'; +import { FormProvider, useForm } from 'react-hook-form'; + +import { putDirectionsAction } from '@/actions/about'; +import Fieldset from '@/components/form/Fieldset'; +import LanguagePicker from '@/components/form/LanguagePicker'; +import Form from '@/components/form/Form'; +import HTMLEditor from '@/components/form/html/HTMLEditor'; +import PageLayout from '@/components/layout/pageLayout/PageLayout'; +import { useRouter } from '@/i18n/routing'; +import { Direction } from '@/types/about'; +import { Language, WithLanguage } from '@/types/language'; +import { errorToStr } from '@/utils/error'; +import { getPath } from '@/utils/page'; +import { directions } from '@/utils/segmentNode'; +import { handleServerAction } from '@/utils/serverActionError'; +import { errorToast, successToast } from '@/utils/toast'; + +const directionsPath = getPath(directions); + +interface FormData { + htmlKo: string; + htmlEn: string; +} + +export default function DirectionEditor({ data }: { data: WithLanguage }) { + const router = useRouter(); + const formMethods = useForm({ + defaultValues: { htmlKo: data.ko.description, htmlEn: data.en.description }, + }); + const [language, setLanguage] = useState('ko'); + + const { handleSubmit } = formMethods; + + const onCancel = () => router.push(directionsPath); + + const onSubmit = handleSubmit(async (formData) => { + try { + handleServerAction( + await putDirectionsAction(data.ko.id, { + koDescription: formData.htmlKo, + enDescription: formData.htmlEn, + }), + ); + successToast('찾아오는 길을 수정했습니다.'); + } catch (e) { + errorToast(errorToStr(e)); + } + }); + + return ( + + +
+ + + + {language === 'ko' && } + {language === 'en' && } + + + +
+
+ ); +} diff --git a/app/[locale]/about/directions/edit/DirectionsEditPageContent.tsx b/app/[locale]/about/directions/edit/DirectionsEditPageContent.tsx deleted file mode 100644 index 919de5027..000000000 --- a/app/[locale]/about/directions/edit/DirectionsEditPageContent.tsx +++ /dev/null @@ -1,51 +0,0 @@ -'use client'; - -import { putDirectionsAction } from '@/actions/about'; -import BasicEditor, { BasicEditorContent } from '@/components/editor/BasicEditor'; -import PageLayout from '@/components/layout/pageLayout/PageLayout'; -import { useRouter } from '@/i18n/routing'; -import { Direction } from '@/types/about'; -import { WithLanguage } from '@/types/language'; -import { errorToStr } from '@/utils/error'; -import { validateBasicForm } from '@/utils/formValidation'; -import { getPath } from '@/utils/page'; -import { directions } from '@/utils/segmentNode'; -import { handleServerAction } from '@/utils/serverActionError'; -import { errorToast, successToast } from '@/utils/toast'; - -const directionsPath = getPath(directions); - -export default function DirectionsEditPageContent({ data }: { data: WithLanguage }) { - const router = useRouter(); - - const handleCancel = () => router.push(directionsPath); - - const handleSubmit = async (content: BasicEditorContent) => { - validateBasicForm(content); - - const newData = { - koDescription: content.description.ko, - enDescription: content.description.en, - }; - - try { - handleServerAction(await putDirectionsAction(data.ko.id, newData)); - successToast('찾아오는 길을 수정했습니다.'); - } catch (e) { - errorToast(errorToStr(e)); - } - }; - - return ( - - - - ); -} diff --git a/app/[locale]/about/directions/edit/page.tsx b/app/[locale]/about/directions/edit/page.tsx index eb2c41891..650308e74 100644 --- a/app/[locale]/about/directions/edit/page.tsx +++ b/app/[locale]/about/directions/edit/page.tsx @@ -1,7 +1,7 @@ import { getDirections } from '@/apis/v2/about/directions'; import { findItemBySearchParam } from '@/utils/findSelectedItem'; -import DirectionsEditPageContent from './DirectionsEditPageContent'; +import DirectionEditor from './DirectionEditor'; interface DirectionsEditPageProps { searchParams: { selected?: string }; @@ -16,5 +16,5 @@ export default async function DirectionsEditPage({ searchParams }: DirectionsEdi searchParams.selected, ) || directionList[0]; - return ; + return ; } diff --git a/app/[locale]/about/facilities/FacilitiesList.tsx b/app/[locale]/about/facilities/FacilitiesList.tsx index 2b9da042b..ff14cbe3e 100644 --- a/app/[locale]/about/facilities/FacilitiesList.tsx +++ b/app/[locale]/about/facilities/FacilitiesList.tsx @@ -4,7 +4,7 @@ import { deleteFacilityAction } from '@/actions/about'; import { DeleteButton, EditButton } from '@/components/common/Buttons'; import ImageWithFallback from '@/components/common/ImageWithFallback'; import LoginVisible from '@/components/common/LoginVisible'; -import HTMLViewer from '@/components/editor/HTMLViewer'; +import HTMLViewer from '@/components/form/html/HTMLViewer'; import Distance from '@/public/image/distance.svg'; import { Facility } from '@/types/about'; import { errorToStr } from '@/utils/error'; diff --git a/app/[locale]/about/facilities/create/page.tsx b/app/[locale]/about/facilities/create/page.tsx index 0221bbcc0..91ff24ee3 100644 --- a/app/[locale]/about/facilities/create/page.tsx +++ b/app/[locale]/about/facilities/create/page.tsx @@ -1,13 +1,20 @@ 'use client'; -import { postFacilityAction } from '@/actions/about'; -import FacilityEditor, { FacilityEditorContent } from '@/components/editor/FacilityEditor'; +import { Fragment, useState } from 'react'; +import { FormProvider, useForm } from 'react-hook-form'; + +import { postFacilityAction, putFacilityAction } from '@/actions/about'; +import Fieldset from '@/components/form/Fieldset'; +import LanguagePicker from '@/components/form/LanguagePicker'; +import { PostEditorImage } from '@/components/form/types'; +import Form from '@/components/form/Form'; +import HTMLEditor from '@/components/form/html/HTMLEditor'; import PageLayout from '@/components/layout/pageLayout/PageLayout'; import { useRouter } from '@/i18n/routing'; -import { WithLanguage } from '@/types/language'; +import { Facility } from '@/types/about'; +import { Language, WithLanguage } from '@/types/language'; import { errorToStr } from '@/utils/error'; import { contentToFormData } from '@/utils/formData'; -import { validateFacilityForm } from '@/utils/formValidation'; import { getPath } from '@/utils/page'; import { facilities } from '@/utils/segmentNode'; import { handleServerAction } from '@/utils/serverActionError'; @@ -15,38 +22,68 @@ import { errorToast, successToast } from '@/utils/toast'; const facilitiesPath = getPath(facilities); -export default function FacilitiesCreatePage() { +interface FormData extends WithLanguage { + imageURL: PostEditorImage | null; +} + +export default function FacilityCreator() { const router = useRouter(); + const formMethods = useForm({ + defaultValues: { + ko: { name: '', description: '', locations: [] }, + en: { name: '', description: '', locations: [] }, + }, + }); + const [selectedLanguage, setSelectedLanguage] = useState('ko'); - const handleCancel = () => router.push(facilitiesPath); + const { handleSubmit } = formMethods; - const handleSubmit = async (content: WithLanguage) => { - validateFacilityForm(content); - const formData = contentToFormData('CREATE', { - requestObject: getRequestObject(content), - image: content.ko.mainImage, - }); + const onCancel = () => router.push(facilitiesPath); + const onSubmit = handleSubmit(async (_formData) => { try { + const formData = contentToFormData('CREATE', { + requestObject: _formData, + image: _formData.imageURL, + }); handleServerAction(await postFacilityAction(formData)); successToast('시설 안내를 추가했습니다.'); } catch (e) { errorToast(errorToStr(e)); } - }; + }); return ( - + + + + {['ko', 'en'].map( + (language) => + language === selectedLanguage && ( + +
+ +
+
+ +
+
+ +
+
+ ), + )} + +
+ + +
+ + +
); } - -const getRequestObject = (content: WithLanguage) => { - const mainImage = undefined; // 이미지는 따로 보내야 하므로 requestObj에서 제외 - - return { - ko: { ...content.ko, mainImage }, - en: { ...content.en, mainImage }, - }; -}; diff --git a/app/[locale]/about/facilities/edit/FacilitiesEditPageContent.tsx b/app/[locale]/about/facilities/edit/FacilitiesEditPageContent.tsx deleted file mode 100644 index 587483eaf..000000000 --- a/app/[locale]/about/facilities/edit/FacilitiesEditPageContent.tsx +++ /dev/null @@ -1,60 +0,0 @@ -'use client'; - -import { putFacilityAction } from '@/actions/about'; -import FacilityEditor, { FacilityEditorContent } from '@/components/editor/FacilityEditor'; -import PageLayout from '@/components/layout/pageLayout/PageLayout'; -import { useRouter } from '@/i18n/routing'; -import { Facility } from '@/types/about'; -import { WithLanguage } from '@/types/language'; -import { errorToStr } from '@/utils/error'; -import { contentToFormData } from '@/utils/formData'; -import { validateFacilityForm } from '@/utils/formValidation'; -import { getPath } from '@/utils/page'; -import { facilities } from '@/utils/segmentNode'; -import { handleServerAction } from '@/utils/serverActionError'; -import { errorToast, successToast } from '@/utils/toast'; - -const facilitiesPath = getPath(facilities); - -export default function FacilitiesEditPageContent({ data }: { data: WithLanguage }) { - const router = useRouter(); - - const handleCancel = () => router.push(facilitiesPath); - - const handleSubmit = async (content: WithLanguage) => { - validateFacilityForm(content); - const formData = contentToFormData('EDIT', { - requestObject: getRequestObject( - content, - data.ko.imageURL !== null && content.ko.mainImage === null, - ), - image: content.ko.mainImage, - }); - - try { - handleServerAction(await putFacilityAction(data.ko.id, formData)); - successToast('시설 안내를 수정했습니다.'); - } catch (e) { - errorToast(errorToStr(e)); - } - }; - - return ( - - - - ); -} - -const getRequestObject = (content: WithLanguage, removeImage: boolean) => { - const mainImage = undefined; // 이미지는 따로 보내야 하므로 requestObj에서 제외 - - return { - ko: { ...content.ko, mainImage }, - en: { ...content.en, mainImage }, - removeImage, - }; -}; diff --git a/app/[locale]/about/facilities/edit/FacilityEditor.tsx b/app/[locale]/about/facilities/edit/FacilityEditor.tsx new file mode 100644 index 000000000..1fb4d4405 --- /dev/null +++ b/app/[locale]/about/facilities/edit/FacilityEditor.tsx @@ -0,0 +1,96 @@ +'use client'; + +import { useState } from 'react'; +import { FormProvider, useForm } from 'react-hook-form'; + +import { putFacilityAction } from '@/actions/about'; +import Fieldset from '@/components/form/Fieldset'; +import LanguagePicker from '@/components/form/LanguagePicker'; +import { PostEditorImage } from '@/components/form/types'; +import Form from '@/components/form/Form'; +import HTMLEditor from '@/components/form/html/HTMLEditor'; +import PageLayout from '@/components/layout/pageLayout/PageLayout'; +import { useRouter } from '@/i18n/routing'; +import { Facility } from '@/types/about'; +import { Language, WithLanguage } from '@/types/language'; +import { errorToStr } from '@/utils/error'; +import { contentToFormData } from '@/utils/formData'; +import { getPath } from '@/utils/page'; +import { facilities } from '@/utils/segmentNode'; +import { handleServerAction } from '@/utils/serverActionError'; +import { errorToast, successToast } from '@/utils/toast'; + +const facilitiesPath = getPath(facilities); + +interface FormData extends WithLanguage { + imageURL: PostEditorImage | null; +} + +export default function FacilityEditor({ data }: { data: WithLanguage }) { + const router = useRouter(); + const formMethods = useForm({ + defaultValues: { + ...data, + imageURL: data.ko.imageURL ? { type: 'UPLOADED_IMAGE', url: data.ko.imageURL } : null, + }, + }); + const [selectedLanguage, setSelectedLanguage] = useState('ko'); + + const { handleSubmit } = formMethods; + + const onCancel = () => router.push(facilitiesPath); + + const onSubmit = handleSubmit(async (formData) => { + try { + handleServerAction( + await putFacilityAction( + data.ko.id, + contentToFormData('EDIT', { + requestObject: { + ...formData, + removeImage: data.ko.imageURL !== null && formData.imageURL === null, + }, + image: formData.imageURL, + }), + ), + ); + successToast('시설 안내를 수정했습니다.'); + } catch (e) { + errorToast(errorToStr(e)); + } + }); + + return ( + + + + + {['ko', 'en'].map( + (language) => + language === selectedLanguage && ( + <> +
+ +
+
+ +
+
+ +
+ + ), + )} + +
+ + +
+ + +
+
+ ); +} diff --git a/app/[locale]/about/facilities/edit/page.tsx b/app/[locale]/about/facilities/edit/page.tsx index ef78e5c37..f91f90109 100644 --- a/app/[locale]/about/facilities/edit/page.tsx +++ b/app/[locale]/about/facilities/edit/page.tsx @@ -1,7 +1,6 @@ import { getFacilities } from '@/apis/v2/about/facilities'; import { findItemBySearchParam } from '@/utils/findSelectedItem'; - -import FacilitiesEditPageContent from './FacilitiesEditPageContent'; +import FacilityEditor from './FacilityEditor'; interface FacilitiesEditPageProps { searchParams: { id: string }; @@ -16,5 +15,5 @@ export default async function FacilitiesEditPage({ searchParams }: FacilitiesEdi searchParams.id, ) || facilities[0]; - return ; + return ; } diff --git a/app/[locale]/about/future-careers/CareerCompanies.tsx b/app/[locale]/about/future-careers/CareerCompanies.tsx index 81266363c..dc00174f4 100644 --- a/app/[locale]/about/future-careers/CareerCompanies.tsx +++ b/app/[locale]/about/future-careers/CareerCompanies.tsx @@ -1,7 +1,8 @@ 'use client'; import { useTranslations } from 'next-intl'; -import { useReducer, useState } from 'react'; +import { useReducer } from 'react'; +import { FormProvider, useForm } from 'react-hook-form'; import { deleteCareerCompanyAction, @@ -10,15 +11,9 @@ import { } from '@/actions/about'; import { BlackButton, DeleteButton, GrayButton, OrangeButton } from '@/components/common/Buttons'; import LoginVisible from '@/components/common/LoginVisible'; -import { buildPostHandler, EditAction } from '@/components/editor/common/ActionButtons'; -import BasicTextInput from '@/components/editor/common/BasicTextInput'; -import AlertModal from '@/components/modal/AlertModal'; +import Form from '@/components/form/Form'; import { FutureCareers } from '@/types/about'; import { errorToStr } from '@/utils/error'; -import { validateCareerCompanyForm } from '@/utils/formValidation'; -import useEditorContent from '@/utils/hooks/useEditorContent'; -import useModal from '@/utils/hooks/useModal'; -import { isNumber } from '@/utils/number'; import { handleServerAction } from '@/utils/serverActionError'; import { errorToast, successToast } from '@/utils/toast'; @@ -27,9 +22,8 @@ export default function CareerCompanies({ companies }: { companies: FutureCareer const [showCreateForm, toggleCreateForm] = useReducer((x) => !x, false); - const handleCreate = async (content: CareerCompanyEditorContent) => { + const onCreate = async (content: CareerCompanyFormData) => { try { - validateCareerCompanyForm(content); handleServerAction(await postCareerCompanyAction(content)); toggleCreateForm(); successToast('졸업생 창업 기업을 추가했습니다.'); @@ -51,11 +45,7 @@ export default function CareerCompanies({ companies }: { companies: FutureCareer
- {showCreateForm && ( - - )} + {showCreateForm && }
    {companies.map((company, index) => ( @@ -93,9 +83,8 @@ interface CompanyTableRowProps { function CompanyTableRow({ index, company }: CompanyTableRowProps) { const [edit, toggleEdit] = useReducer((x) => !x, false); - const handleSubmit = async (content: CareerCompanyEditorContent) => { + const onSubmit = async (content: CareerCompanyFormData) => { try { - validateCareerCompanyForm(content); handleServerAction(await putCareerCompanyAction(company.id, { id: company.id, ...content })); toggleEdit(); successToast('졸업생 창업 기업을 수정했습니다.'); @@ -108,7 +97,8 @@ function CompanyTableRow({ index, company }: CompanyTableRowProps) { ) : ( @@ -159,63 +149,51 @@ function CareerCompanyViewer({ ); } -export interface CareerCompanyEditorContent { +export interface CareerCompanyFormData { name: string; - url?: string; + url: string; year: number; } -const DEFAULT_COMPANY = { name: '', url: '', year: 0 }; - function CareerCompanyEditor({ index, company, - actions, + onSubmit, + onCancel, }: Partial & { - actions: EditAction; + onSubmit: (formData: CareerCompanyFormData) => Promise; + onCancel: () => void; }) { - const { content, setContentByKey } = useEditorContent(company ?? DEFAULT_COMPANY); - const { openModal } = useModal(); - const [requesting, setRequesting] = useState(false); - - const handleCancel = () => { - if (content.name || content.url || content.year) - openModal(); - else { - actions.onCancel(); - } - }; + const formMethods = useForm({ + defaultValues: { name: company?.name ?? '', url: company?.url ?? '', year: company?.year }, + }); + const { handleSubmit } = formMethods; return ( -
  1. -

    {index}

    -
    - -
    -
    - -
    -
    - isNumber(text) && setContentByKey('year')(Number(text))} - maxWidth="w-full" - /> -
    - -
    - - content, actions.onSubmit)} + +
  2. +

    {index}

    +
    + +
    +
    + +
    +
    +
    - -
  3. + +
    + + +
    +
    + + ); } diff --git a/app/[locale]/about/future-careers/CareerStat.tsx b/app/[locale]/about/future-careers/CareerStat.tsx index c7100fcbe..ab515ada4 100644 --- a/app/[locale]/about/future-careers/CareerStat.tsx +++ b/app/[locale]/about/future-careers/CareerStat.tsx @@ -4,8 +4,8 @@ import { useTranslations } from 'next-intl'; import { useState } from 'react'; import { EditButton, OrangeButton } from '@/components/common/Buttons'; -import Dropdown from '@/components/common/form/Dropdown'; import LoginVisible from '@/components/common/LoginVisible'; +import Dropdown from '@/components/form/legacy/Dropdown'; import { Link } from '@/i18n/routing'; import { FutureCareers } from '@/types/about'; import { getPath } from '@/utils/page'; diff --git a/app/[locale]/about/future-careers/components/CareerStatEditor.tsx b/app/[locale]/about/future-careers/components/CareerStatEditor.tsx new file mode 100644 index 000000000..990fdc7f4 --- /dev/null +++ b/app/[locale]/about/future-careers/components/CareerStatEditor.tsx @@ -0,0 +1,83 @@ +'use client'; + +import { FutureCareers } from '@/types/about'; +import Fieldset from '@/components/form/Fieldset'; +import { FormProvider, useForm, useFormContext, useWatch } from 'react-hook-form'; + +import Form from '@/components/form/Form'; +import { getPath } from '@/utils/page'; +import { futureCareers } from '@/utils/segmentNode'; +import { useRouter } from '@/i18n/routing'; +import { CareerStat } from '@/apis/v2/about/future-careers/stats'; +import { degreeList, companyList, COMPANY_MAP } from '@/apis/v2/about/future-careers/types'; + +interface Props { + defaultValues?: CareerStat; + onSubmit: (formData: CareerStat) => Promise; +} + +const DEGREE_MAP = { bachelor: '학부', master: '석사', doctor: '박사' } as const; + +const careerPath = getPath(futureCareers); + +const DEFAULT_STATS: CareerStat = { + year: new Date().getFullYear() + 1, + statList: companyList.map((company) => ({ career: company, bachelor: 0, master: 0, doctor: 0 })), +}; + +export default function CareerStatEditor({ onSubmit, defaultValues }: Props) { + const formMethods = useForm({ defaultValues: defaultValues ?? DEFAULT_STATS }); + const { handleSubmit } = formMethods; + + const router = useRouter(); + const onCancel = () => router.push(careerPath); + + return ( + +
    +
    + +
    + +
    + + +
    + + +
    + ); +} + +function TableHeader() { + return ( +
    +
    + {degreeList.map((degree) => ( +
    +

    {DEGREE_MAP[degree]}

    +
    + ))} +
    + ); +} + +function TableBody() { + const { control } = useFormContext(); + const statList = useWatch({ name: 'statList', control }); + + return statList.map((stat, idx) => { + return ( +
    +
    + {COMPANY_MAP[stat.career]} +
    + {degreeList.map((degree) => ( +
    + +
    + ))} +
    + ); + }); +} diff --git a/app/[locale]/about/future-careers/description/edit/CareerDescriptionEditContent.tsx b/app/[locale]/about/future-careers/description/edit/CareerDescriptionEditContent.tsx deleted file mode 100644 index 4a01cc8c4..000000000 --- a/app/[locale]/about/future-careers/description/edit/CareerDescriptionEditContent.tsx +++ /dev/null @@ -1,47 +0,0 @@ -'use client'; - -import { putCareerDescriptionAction } from '@/actions/about'; -import BasicEditor, { BasicEditorContent } from '@/components/editor/BasicEditor'; -import PageLayout from '@/components/layout/pageLayout/PageLayout'; -import { useRouter } from '@/i18n/routing'; -import { WithLanguage } from '@/types/language'; -import { errorToStr } from '@/utils/error'; -import { validateBasicForm } from '@/utils/formValidation'; -import { getPath } from '@/utils/page'; -import { studentClubs } from '@/utils/segmentNode'; -import { handleServerAction } from '@/utils/serverActionError'; -import { errorToast, successToast } from '@/utils/toast'; - -const clubPath = getPath(studentClubs); - -export default function CareerDescriptionEditPageContent({ data }: { data: WithLanguage }) { - const router = useRouter(); - - const handleCancel = () => router.push(clubPath); - - const handleSubmit = async (content: BasicEditorContent) => { - validateBasicForm(content); - - const newData = { - koDescription: content.description.ko, - enDescription: content.description.en, - }; - - try { - handleServerAction(await putCareerDescriptionAction(newData)); - successToast('졸업생 진로 본문을 수정했습니다.'); - } catch (e) { - errorToast(errorToStr(e)); - } - }; - - return ( - - - - ); -} diff --git a/app/[locale]/about/future-careers/description/edit/CareerDescriptionEditor.tsx b/app/[locale]/about/future-careers/description/edit/CareerDescriptionEditor.tsx new file mode 100644 index 000000000..d94372761 --- /dev/null +++ b/app/[locale]/about/future-careers/description/edit/CareerDescriptionEditor.tsx @@ -0,0 +1,58 @@ +'use client'; + +import { putCareerDescriptionAction } from '@/actions/about'; +import Fieldset from '@/components/form/Fieldset'; +import LanguagePicker from '@/components/form/LanguagePicker'; +import Form from '@/components/form/Form'; +import HTMLEditor from '@/components/form/html/HTMLEditor'; +import PageLayout from '@/components/layout/pageLayout/PageLayout'; +import { useRouter } from '@/i18n/routing'; +import { Language, WithLanguage } from '@/types/language'; +import { errorToStr } from '@/utils/error'; +import { getPath } from '@/utils/page'; +import { studentClubs } from '@/utils/segmentNode'; +import { handleServerAction } from '@/utils/serverActionError'; +import { errorToast, successToast } from '@/utils/toast'; +import { useState } from 'react'; +import { useForm, FormProvider } from 'react-hook-form'; + +const clubPath = getPath(studentClubs); + +export default function CareerDescriptionEditor({ data }: { data: WithLanguage }) { + const router = useRouter(); + const formMethods = useForm>({ defaultValues: data }); + const [language, setLanguage] = useState('ko'); + + const { handleSubmit } = formMethods; + + const onCancel = () => router.push(clubPath); + + const onSubmit = handleSubmit(async (formData) => { + try { + handleServerAction( + await putCareerDescriptionAction({ + koDescription: formData.ko, + enDescription: formData.en, + }), + ); + successToast('졸업생 진로 본문을 수정했습니다.'); + } catch (e) { + errorToast(errorToStr(e)); + } + }); + + return ( + + +
    + + + {language === 'ko' && } + {language === 'en' && } + + + +
    +
    + ); +} diff --git a/app/[locale]/about/future-careers/description/edit/page.tsx b/app/[locale]/about/future-careers/description/edit/page.tsx index dd9474553..c738f3abf 100644 --- a/app/[locale]/about/future-careers/description/edit/page.tsx +++ b/app/[locale]/about/future-careers/description/edit/page.tsx @@ -1,11 +1,9 @@ import { getFutureCareeres } from '@/apis/v1/about/future-careers'; -import CareerDescriptionEditPageContent from './CareerDescriptionEditContent'; +import CareerDescriptionEditor from './CareerDescriptionEditor'; export default async function CareerDescriptionEditPage() { const [koData, enData] = await Promise.all([getFutureCareeres('ko'), getFutureCareeres('en')]); - return ( - - ); + return ; } diff --git a/app/[locale]/about/future-careers/page.tsx b/app/[locale]/about/future-careers/page.tsx index aaf033b25..5256e6eba 100644 --- a/app/[locale]/about/future-careers/page.tsx +++ b/app/[locale]/about/future-careers/page.tsx @@ -3,7 +3,7 @@ export const dynamic = 'force-dynamic'; import { getFutureCareeres } from '@/apis/v1/about/future-careers'; import { EditButton } from '@/components/common/Buttons'; import LoginVisible from '@/components/common/LoginVisible'; -import HTMLViewer from '@/components/editor/HTMLViewer'; +import HTMLViewer from '@/components/form/html/HTMLViewer'; import PageLayout from '@/components/layout/pageLayout/PageLayout'; import { Language } from '@/types/language'; import { getMetadata } from '@/utils/metadata'; diff --git a/app/[locale]/about/future-careers/stat/create/page.tsx b/app/[locale]/about/future-careers/stat/create/page.tsx index 942004f38..3f9bac276 100644 --- a/app/[locale]/about/future-careers/stat/create/page.tsx +++ b/app/[locale]/about/future-careers/stat/create/page.tsx @@ -1,25 +1,17 @@ 'use client'; import { postCareerStatAction } from '@/actions/about'; -import CareerStatEditor, { CareerStatEditorContent } from '@/components/editor/CareerStatEditor'; import PageLayout from '@/components/layout/pageLayout/PageLayout'; -import { useRouter } from '@/i18n/routing'; import { errorToStr } from '@/utils/error'; -import { getPath } from '@/utils/page'; -import { futureCareers } from '@/utils/segmentNode'; import { handleServerAction } from '@/utils/serverActionError'; import { errorToast, successToast } from '@/utils/toast'; - -const careerPath = getPath(futureCareers); +import { CareerStat } from '@/apis/v2/about/future-careers/stats'; +import CareerStatEditor from '../../components/CareerStatEditor'; export default function CareerStatCreatePage() { - const router = useRouter(); - - const handleCancel = () => router.push(careerPath); - - const handleSubmit = async (content: CareerStatEditorContent) => { + const onSubmit = async (formData: CareerStat) => { try { - handleServerAction(await postCareerStatAction(content)); + handleServerAction(await postCareerStatAction(formData)); successToast('졸업생 진로 현황을 추가했습니다.'); } catch (e) { errorToast(errorToStr(e)); @@ -28,9 +20,7 @@ export default function CareerStatCreatePage() { return ( - + ); } diff --git a/app/[locale]/about/future-careers/stat/edit/CareerStatEditPageContent.tsx b/app/[locale]/about/future-careers/stat/edit/CareerStatEditPageContent.tsx index ae29eb52e..65ec53fd9 100644 --- a/app/[locale]/about/future-careers/stat/edit/CareerStatEditPageContent.tsx +++ b/app/[locale]/about/future-careers/stat/edit/CareerStatEditPageContent.tsx @@ -1,7 +1,7 @@ 'use client'; import { putCareerStatAction } from '@/actions/about'; -import CareerStatEditor, { CareerStatEditorContent } from '@/components/editor/CareerStatEditor'; +import { CareerStat } from '@/apis/v2/about/future-careers/stats'; import PageLayout from '@/components/layout/pageLayout/PageLayout'; import { useRouter } from '@/i18n/routing'; import { FutureCareers } from '@/types/about'; @@ -10,19 +10,27 @@ import { getPath } from '@/utils/page'; import { futureCareers } from '@/utils/segmentNode'; import { handleServerAction } from '@/utils/serverActionError'; import { errorToast, successToast } from '@/utils/toast'; - -const careerPath = getPath(futureCareers); +import CareerStatEditor from '../../components/CareerStatEditor'; +import { COMPANY_MAP, companyList } from '@/apis/v2/about/future-careers/types'; export default function CareerStatEditPageContent({ data, }: { data: FutureCareers['stat'][number]; }) { - const router = useRouter(); - - const handleCancel = () => router.push(careerPath); + const careerStat = { + year: data.year, + statList: companyList.map((company) => { + return { + career: company, + bachelor: data.bachelor.find((x) => x.name === COMPANY_MAP[company])?.count ?? 0, + master: data.master.find((x) => x.name === COMPANY_MAP[company])?.count ?? 0, + doctor: data.doctor.find((x) => x.name === COMPANY_MAP[company])?.count ?? 0, + }; + }), + }; - const handleSubmit = async (content: CareerStatEditorContent) => { + const onSubmit = async (content: CareerStat) => { try { handleServerAction(await putCareerStatAction(content)); successToast('졸업생 진로 현황을 수정했습니다.'); @@ -33,10 +41,7 @@ export default function CareerStatEditPageContent({ return ( - + ); } diff --git a/app/[locale]/about/greetings/edit/page.tsx b/app/[locale]/about/greetings/edit/page.tsx index 78a347d9d..14575bf51 100644 --- a/app/[locale]/about/greetings/edit/page.tsx +++ b/app/[locale]/about/greetings/edit/page.tsx @@ -1,10 +1,10 @@ import { getGreetings } from '@/apis/v1/about/greetings'; import { greetings } from '@/utils/segmentNode'; -import AboutEditPageContent from '../../AboutEditPageContent'; +import AboutEditor from '../../components/AboutEditor'; export default async function GreetingsEditPage() { const [koData, enData] = await Promise.all([getGreetings('ko'), getGreetings('en')]); - return ; + return ; } diff --git a/app/[locale]/about/greetings/page.tsx b/app/[locale]/about/greetings/page.tsx index afe043a32..df16f0e6a 100644 --- a/app/[locale]/about/greetings/page.tsx +++ b/app/[locale]/about/greetings/page.tsx @@ -5,7 +5,7 @@ import Image from 'next/image'; import { getGreetings } from '@/apis/v1/about/greetings'; import { EditButton } from '@/components/common/Buttons'; import LoginVisible from '@/components/common/LoginVisible'; -import HTMLViewer from '@/components/editor/HTMLViewer'; +import HTMLViewer from '@/components/form/html/HTMLViewer'; import PageLayout from '@/components/layout/pageLayout/PageLayout'; import { Language } from '@/types/language'; import { getMetadata } from '@/utils/metadata'; diff --git a/app/[locale]/about/history/edit/page.tsx b/app/[locale]/about/history/edit/page.tsx index 4502ff89f..d0609e4f6 100644 --- a/app/[locale]/about/history/edit/page.tsx +++ b/app/[locale]/about/history/edit/page.tsx @@ -1,10 +1,10 @@ import { getHistory } from '@/apis/v1/about/history'; import { history } from '@/utils/segmentNode'; -import AboutEditPageContent from '../../AboutEditPageContent'; +import AboutEditor from '../../components/AboutEditor'; export default async function HistoryEditPage() { const [koData, enData] = await Promise.all([getHistory('ko'), getHistory('en')]); - return ; + return ; } diff --git a/app/[locale]/about/history/page.tsx b/app/[locale]/about/history/page.tsx index 0c1db6b6b..803e9cd1e 100644 --- a/app/[locale]/about/history/page.tsx +++ b/app/[locale]/about/history/page.tsx @@ -3,7 +3,7 @@ export const dynamic = 'force-dynamic'; import { getHistory } from '@/apis/v1/about/history'; import { EditButton } from '@/components/common/Buttons'; import LoginVisible from '@/components/common/LoginVisible'; -import HTMLViewer from '@/components/editor/HTMLViewer'; +import HTMLViewer from '@/components/form/html/HTMLViewer'; import PageLayout from '@/components/layout/pageLayout/PageLayout'; import history_image from '@/public/image/about/history.png'; import { Language } from '@/types/language'; diff --git a/app/[locale]/about/overview/edit/page.tsx b/app/[locale]/about/overview/edit/page.tsx index 6deb9bd4c..ad7cd7555 100644 --- a/app/[locale]/about/overview/edit/page.tsx +++ b/app/[locale]/about/overview/edit/page.tsx @@ -1,10 +1,10 @@ import { getOverview } from '@/apis/v1/about/overview'; import { overview } from '@/utils/segmentNode'; -import AboutEditPageContent from '../../AboutEditPageContent'; +import AboutEditor from '../../components/AboutEditor'; export default async function OverviewEditPage() { const [koData, enData] = await Promise.all([getOverview('ko'), getOverview('en')]); - return ; + return ; } diff --git a/app/[locale]/about/overview/page.tsx b/app/[locale]/about/overview/page.tsx index 182876dfa..50e7e8329 100644 --- a/app/[locale]/about/overview/page.tsx +++ b/app/[locale]/about/overview/page.tsx @@ -7,7 +7,7 @@ import { getOverview } from '@/apis/v1/about/overview'; import Attachments from '@/components/common/Attachments'; import { EditButton } from '@/components/common/Buttons'; import LoginVisible from '@/components/common/LoginVisible'; -import HTMLViewer from '@/components/editor/HTMLViewer'; +import HTMLViewer from '@/components/form/html/HTMLViewer'; import PageLayout from '@/components/layout/pageLayout/PageLayout'; import brochure1 from '@/public/image/about/brochure1.png'; import brochure2 from '@/public/image/about/brochure2.png'; diff --git a/app/[locale]/about/student-clubs/ClubDetails.tsx b/app/[locale]/about/student-clubs/ClubDetails.tsx index 8e2200c12..114f61d96 100644 --- a/app/[locale]/about/student-clubs/ClubDetails.tsx +++ b/app/[locale]/about/student-clubs/ClubDetails.tsx @@ -4,7 +4,7 @@ import { deleteClubAction } from '@/actions/about'; import { DeleteButton, EditButton } from '@/components/common/Buttons'; import LoginVisible from '@/components/common/LoginVisible'; import SelectionTitle from '@/components/common/selection/SelectionTitle'; -import HTMLViewer from '@/components/editor/HTMLViewer'; +import HTMLViewer from '@/components/form/html/HTMLViewer'; import { Club } from '@/types/about'; import { Language, WithLanguage } from '@/types/language'; import { errorToStr } from '@/utils/error'; diff --git a/app/[locale]/about/student-clubs/components/ClubEditor.tsx b/app/[locale]/about/student-clubs/components/ClubEditor.tsx new file mode 100644 index 000000000..9ce536085 --- /dev/null +++ b/app/[locale]/about/student-clubs/components/ClubEditor.tsx @@ -0,0 +1,60 @@ +'use client'; + +import Fieldset from '@/components/form/Fieldset'; +import LanguagePicker from '@/components/form/LanguagePicker'; +import { PostEditorImage } from '@/components/form/types'; +import Form from '@/components/form/Form'; +import HTMLEditor from '@/components/form/html/HTMLEditor'; +import { useRouter } from '@/i18n/routing'; +import { Language, WithLanguage } from '@/types/language'; +import { getPath } from '@/utils/page'; +import { studentClubs } from '@/utils/segmentNode'; +import { useState } from 'react'; +import { FormProvider, useForm } from 'react-hook-form'; + +const clubPath = getPath(studentClubs); + +export interface ClubFormData extends WithLanguage<{ name: string; description: string }> { + image?: PostEditorImage; +} + +interface Props { + defaultValues?: ClubFormData; + onSubmit: (formData: ClubFormData) => void; +} + +export default function ClubEditor({ defaultValues, onSubmit }: Props) { + const router = useRouter(); + const formMethods = useForm({ defaultValues }); + const { handleSubmit } = formMethods; + const [language, setLanguage] = useState('ko'); + + const onCancel = () => router.push(clubPath); + + return ( + +
    + + + + {language === 'ko' && } + {language === 'en' && } + + + + {language === 'ko' && } + {language === 'en' && } + + + + + + + + + +
    + ); +} diff --git a/app/[locale]/about/student-clubs/create/page.tsx b/app/[locale]/about/student-clubs/create/page.tsx index c04ad9e5d..62b44d282 100644 --- a/app/[locale]/about/student-clubs/create/page.tsx +++ b/app/[locale]/about/student-clubs/create/page.tsx @@ -1,33 +1,17 @@ 'use client'; import { postClubAction } from '@/actions/about'; -import BasicEditor, { BasicEditorContent } from '@/components/editor/BasicEditor'; import PageLayout from '@/components/layout/pageLayout/PageLayout'; -import { useRouter } from '@/i18n/routing'; import { errorToStr } from '@/utils/error'; import { contentToFormData } from '@/utils/formData'; -import { validateBasicForm } from '@/utils/formValidation'; -import { getPath } from '@/utils/page'; -import { studentClubs } from '@/utils/segmentNode'; import { handleServerAction } from '@/utils/serverActionError'; import { errorToast, successToast } from '@/utils/toast'; - -const clubPath = getPath(studentClubs); +import ClubEditor, { ClubFormData } from '../components/ClubEditor'; export default function StudentClubCreatePage() { - const router = useRouter(); - - const handleCancel = () => router.push(clubPath); - - const handleSubmit = async (content: BasicEditorContent) => { - validateBasicForm(content); - - const formData = contentToFormData('CREATE', { - requestObject: getRequestObject(content), - image: content.mainImage, - }); - + const onSubmit = async ({ image, ...requestObject }: ClubFormData) => { try { + const formData = contentToFormData('CREATE', { requestObject, image }); handleServerAction(await postClubAction(formData)); successToast('동아리 소개를 추가했습니다.'); } catch (e) { @@ -37,19 +21,7 @@ export default function StudentClubCreatePage() { return ( - + ); } - -const getRequestObject = (content: BasicEditorContent) => { - return { - ko: { name: content.title.ko, description: content.description.ko }, - en: { name: content.title.en, description: content.description.en }, - }; -}; diff --git a/app/[locale]/about/student-clubs/edit/StudentClubEditPageContent.tsx b/app/[locale]/about/student-clubs/edit/StudentClubEditPageContent.tsx index 0584a3cfc..75cca5025 100644 --- a/app/[locale]/about/student-clubs/edit/StudentClubEditPageContent.tsx +++ b/app/[locale]/about/student-clubs/edit/StudentClubEditPageContent.tsx @@ -1,36 +1,24 @@ 'use client'; import { putClubAction } from '@/actions/about'; -import BasicEditor, { BasicEditorContent } from '@/components/editor/BasicEditor'; import PageLayout from '@/components/layout/pageLayout/PageLayout'; -import { useRouter } from '@/i18n/routing'; import { Club } from '@/types/about'; import { WithLanguage } from '@/types/language'; import { errorToStr } from '@/utils/error'; import { contentToFormData } from '@/utils/formData'; -import { validateBasicForm } from '@/utils/formValidation'; -import { getPath } from '@/utils/page'; -import { studentClubs } from '@/utils/segmentNode'; import { handleServerAction } from '@/utils/serverActionError'; import { errorToast, successToast } from '@/utils/toast'; - -const clubPath = getPath(studentClubs); +import ClubEditor, { ClubFormData } from '../components/ClubEditor'; export default function StudentClubEditPageContent({ data }: { data: WithLanguage }) { - const router = useRouter(); - - const handleCancel = () => router.push(clubPath); - - const handleSubmit = async (content: BasicEditorContent) => { - validateBasicForm(content, { titleRequired: true }); - + const onSubmit = async (_formData: ClubFormData) => { const formData = contentToFormData('EDIT', { - requestObject: getRequestObject( - { ko: data.ko.id, en: data.en.id }, - content, - data.ko.imageURL !== null && content.mainImage === null, - ), - image: content.mainImage, + requestObject: { + ko: _formData.ko, + en: _formData.en, + removeImage: data.ko.imageURL !== null && _formData.image === null, + }, + image: _formData.image, }); try { @@ -43,29 +31,13 @@ export default function StudentClubEditPageContent({ data }: { data: WithLanguag return ( - ); } - -const getRequestObject = ( - ids: WithLanguage, - content: BasicEditorContent, - removeImage: boolean, -) => { - return { - ko: { id: ids.ko, name: content.title.ko, description: content.description.ko }, - en: { id: ids.en, name: content.title.en, description: content.description.en }, - removeImage, - }; -}; diff --git a/app/[locale]/academics/helper/RoadMapButton.tsx b/app/[locale]/academics/components/RoadMapButton.tsx similarity index 100% rename from app/[locale]/academics/helper/RoadMapButton.tsx rename to app/[locale]/academics/components/RoadMapButton.tsx diff --git a/app/[locale]/academics/helper/RoadMapModal.tsx b/app/[locale]/academics/components/RoadMapModal.tsx similarity index 100% rename from app/[locale]/academics/helper/RoadMapModal.tsx rename to app/[locale]/academics/components/RoadMapModal.tsx diff --git a/app/[locale]/academics/helper/courses/AddCourseButton.tsx b/app/[locale]/academics/components/courses/AddCourseButton.tsx similarity index 100% rename from app/[locale]/academics/helper/courses/AddCourseButton.tsx rename to app/[locale]/academics/components/courses/AddCourseButton.tsx diff --git a/app/[locale]/academics/components/courses/AddCourseModal.tsx b/app/[locale]/academics/components/courses/AddCourseModal.tsx new file mode 100644 index 000000000..6144bbeb8 --- /dev/null +++ b/app/[locale]/academics/components/courses/AddCourseModal.tsx @@ -0,0 +1,141 @@ +import { FormProvider, useForm } from 'react-hook-form'; + +import { postCourseAction } from '@/actions/academics'; +import Fieldset from '@/components/form/Fieldset'; +import Form from '@/components/form/Form'; +import ModalFrame from '@/components/modal/ModalFrame'; +import { CLASSIFICATION, Course, GRADE, StudentType } from '@/types/academics'; +import { getKeys } from '@/utils/object'; +import { errorToStr } from '@/utils/error'; +import { handleServerAction } from '@/utils/serverActionError'; +import { errorToast, successToast } from '@/utils/toast'; + +export default function AddCourseModal({ + onClose, + studentType, +}: { + onClose: () => void; + studentType: StudentType; +}) { + const formMethods = useForm({ + defaultValues: { + code: '', + credit: 3, + grade: studentType === 'graduate' ? 0 : 1, + studentType: studentType, + ko: { name: '', description: '', classification: '전공필수' }, + en: { name: '', description: '', classification: 'RM' }, + }, + }); + const { handleSubmit } = formMethods; + + const onSubmit = async (course: Course) => { + try { + handleServerAction(await postCourseAction(course)); + successToast('새 교과목을 추가했습니다.'); + onClose(); + } catch (e) { + errorToast(errorToStr(e)); + } + }; + + return ( + + +
    +

    교과목 추가

    +
    +
    + +
    +
    + +
    +
    +
    + +
    + ({ value, label: value }))} + name="ko.classification" + width="w-[94px]" + /> + ({ value, label: value.toString() }))} + /> + ({ value: idx + 1, label })) + : [{ value: 0, label: GRADE[0] }] + } + name="grade" + width="w-[90px]" + /> +
    +
    + * 교과목 번호는 추후 수정할 수 없습니다. +
    +
    + +
    +
    + +
    +
    +
    +
    +
    +
    +
    + ); +} + +function Button({ text, onClick }: { text: string; onClick: () => void }) { + return ( + + ); +} + +function DropdownFieldset({ + title, + contents, + name, + width, +}: { + title: string; + contents: { value: unknown; label: string }[]; + name: string; + width?: string; +}) { + return ( +
    + +
    + ); +} + +const CREDIT = [1, 2, 3, 4]; diff --git a/app/[locale]/academics/helper/courses/CourseCard.tsx b/app/[locale]/academics/components/courses/CourseCard.tsx similarity index 100% rename from app/[locale]/academics/helper/courses/CourseCard.tsx rename to app/[locale]/academics/components/courses/CourseCard.tsx diff --git a/app/[locale]/academics/helper/courses/CourseCards.tsx b/app/[locale]/academics/components/courses/CourseCards.tsx similarity index 100% rename from app/[locale]/academics/helper/courses/CourseCards.tsx rename to app/[locale]/academics/components/courses/CourseCards.tsx diff --git a/app/[locale]/academics/helper/courses/CourseDetailModal.tsx b/app/[locale]/academics/components/courses/CourseDetailModal.tsx similarity index 95% rename from app/[locale]/academics/helper/courses/CourseDetailModal.tsx rename to app/[locale]/academics/components/courses/CourseDetailModal.tsx index 6f5c7dc64..b93f4edcd 100644 --- a/app/[locale]/academics/helper/courses/CourseDetailModal.tsx +++ b/app/[locale]/academics/components/courses/CourseDetailModal.tsx @@ -39,7 +39,11 @@ export default function CourseDetailModal({ initCourse, onClose }: CourseDetailM
    {isEditMode ? ( - + ) : ( )} diff --git a/app/[locale]/academics/components/courses/CourseEditor.tsx b/app/[locale]/academics/components/courses/CourseEditor.tsx new file mode 100644 index 000000000..77787d2d2 --- /dev/null +++ b/app/[locale]/academics/components/courses/CourseEditor.tsx @@ -0,0 +1,125 @@ +import { ReactNode } from 'react'; +import { FormProvider, useForm } from 'react-hook-form'; + +import { putCourseAction } from '@/actions/academics'; +import Form from '@/components/form/Form'; +import BookmarkIcon from '@/public/image/bookmark_icon.svg'; +import { CLASSIFICATION, ClassificationEn, Course, GRADE } from '@/types/academics'; +import { errorToStr } from '@/utils/error'; +import { handleServerAction } from '@/utils/serverActionError'; +import { errorToast, successToast } from '@/utils/toast'; + +const CREDIT = [1, 2, 3, 4]; + +export default function CourseEditor({ + defaultValues, + toggleEditMode, + setCourse, +}: { + defaultValues: Course; + toggleEditMode: () => void; + setCourse: (course: Course) => void; +}) { + const formMethods = useForm({ defaultValues }); + const { setValue, handleSubmit } = formMethods; + const gradeDropdownContents = defaultValues.grade === 0 ? [GRADE[0]] : GRADE.slice(1); + + const onSubmit = async (course: Course) => { + try { + handleServerAction(await putCourseAction(course)); + successToast('교과목을 수정했습니다.'); + setCourse(course); + toggleEditMode(); + } catch (e) { + errorToast(errorToStr(e)); + } + }; + + return ( + +

    + + +
    errorToast('교과목 코드는 수정할 수 없습니다')} + > + {defaultValues.code} +
    + ({ label: value, value }))} + name="ko.classification" + borderStyle="border-neutral-300" + height="h-8" + width="w-[94px]" + onChange={(value) => setValue('en.classification', value as ClassificationEn)} + /> + ({ label: value.toString(), value }))} + name="credit" + borderStyle="border-neutral-300" + height="h-8" + /> + ({ value: idx, label }))} + name="grade" + borderStyle="border-neutral-300" + height="h-8" + width="w-[90px]" + /> +

    + +
    +
    + 영문 + +
    + +
    +
    + {/* TODO: disabled 처리 */} + + +
    +
    + ); +} + +function Button({ + onClick, + children, + dark = false, +}: { + onClick: () => void; + children?: ReactNode; + dark?: boolean; +}) { + return ( + + ); +} diff --git a/app/[locale]/academics/helper/courses/CourseList.tsx b/app/[locale]/academics/components/courses/CourseList.tsx similarity index 100% rename from app/[locale]/academics/helper/courses/CourseList.tsx rename to app/[locale]/academics/components/courses/CourseList.tsx diff --git a/app/[locale]/academics/helper/courses/CourseListHeader.tsx b/app/[locale]/academics/components/courses/CourseListHeader.tsx similarity index 100% rename from app/[locale]/academics/helper/courses/CourseListHeader.tsx rename to app/[locale]/academics/components/courses/CourseListHeader.tsx diff --git a/app/[locale]/academics/helper/courses/CourseListRow.tsx b/app/[locale]/academics/components/courses/CourseListRow.tsx similarity index 100% rename from app/[locale]/academics/helper/courses/CourseListRow.tsx rename to app/[locale]/academics/components/courses/CourseListRow.tsx diff --git a/app/[locale]/academics/helper/courses/CourseRow.tsx b/app/[locale]/academics/components/courses/CourseRow.tsx similarity index 100% rename from app/[locale]/academics/helper/courses/CourseRow.tsx rename to app/[locale]/academics/components/courses/CourseRow.tsx diff --git a/app/[locale]/academics/helper/courses/CourseToolbar.tsx b/app/[locale]/academics/components/courses/CourseToolbar.tsx similarity index 100% rename from app/[locale]/academics/helper/courses/CourseToolbar.tsx rename to app/[locale]/academics/components/courses/CourseToolbar.tsx diff --git a/app/[locale]/academics/helper/courses/useCourseToolbar.tsx b/app/[locale]/academics/components/courses/useCourseToolbar.tsx similarity index 100% rename from app/[locale]/academics/helper/courses/useCourseToolbar.tsx rename to app/[locale]/academics/components/courses/useCourseToolbar.tsx diff --git a/app/[locale]/academics/components/guide/GuideEditor.tsx b/app/[locale]/academics/components/guide/GuideEditor.tsx new file mode 100644 index 000000000..880ea5a04 --- /dev/null +++ b/app/[locale]/academics/components/guide/GuideEditor.tsx @@ -0,0 +1,41 @@ +'use client'; + +import { FormProvider, useForm } from 'react-hook-form'; + +import Fieldset from '@/components/form/Fieldset'; +import { PostEditorFile } from '@/components/form/types'; +import Form from '@/components/form/Form'; +import HTMLEditor from '@/components/form/html/HTMLEditor'; +import { useRouter } from '@/i18n/routing'; + +export interface GuideFormData { + description: string; + file: PostEditorFile[]; +} + +interface Props { + defaultValues: GuideFormData; + onCancelPath: string; + onSubmit: (formData: GuideFormData) => Promise; +} + +export default function GuideEditor({ defaultValues, onCancelPath, onSubmit }: Props) { + const formMethods = useForm({ defaultValues }); + const { handleSubmit } = formMethods; + const router = useRouter(); + const onCancel = () => router.push(onCancelPath); + + return ( + +
    + + + + + + + + +
    + ); +} diff --git a/app/[locale]/academics/components/guide/GuideEditorBridge.tsx b/app/[locale]/academics/components/guide/GuideEditorBridge.tsx new file mode 100644 index 000000000..47275ec61 --- /dev/null +++ b/app/[locale]/academics/components/guide/GuideEditorBridge.tsx @@ -0,0 +1,40 @@ +'use client'; + +import GuideEditor, { GuideFormData } from '@/app/[locale]/academics/components/guide/GuideEditor'; +import { Guide } from '@/types/academics'; +import { errorToStr } from '@/utils/error'; +import { contentToFormData, getAttachmentDeleteIds } from '@/utils/formData'; +import { handleServerAction } from '@/utils/serverActionError'; +import { errorToast } from '@/utils/toast'; + +interface Props { + data: Guide; + serverAction: (formData: FormData) => Promise; + path: string; +} + +export default function GuideEditBridge({ data, serverAction, path }: Props) { + const onSubmit = async (_formData: GuideFormData) => { + try { + const deleteIds = getAttachmentDeleteIds(_formData.file, data.attachments); + const formData = contentToFormData('EDIT', { + requestObject: { description: _formData.description, deleteIds }, + attachments: _formData.file, + }); + handleServerAction(await serverAction(formData)); + } catch (e) { + errorToast(errorToStr(e)); + } + }; + + return ( + ({ type: 'UPLOADED_FILE', file })), + }} + onCancelPath={path} + onSubmit={onSubmit} + /> + ); +} diff --git a/app/[locale]/academics/helper/guide/GuidePageContent.tsx b/app/[locale]/academics/components/guide/GuidePageContent.tsx similarity index 93% rename from app/[locale]/academics/helper/guide/GuidePageContent.tsx rename to app/[locale]/academics/components/guide/GuidePageContent.tsx index b895716a7..30d301b8d 100644 --- a/app/[locale]/academics/helper/guide/GuidePageContent.tsx +++ b/app/[locale]/academics/components/guide/GuidePageContent.tsx @@ -3,7 +3,7 @@ import Attachments from '@/components/common/Attachments'; import { BlackButton } from '@/components/common/Buttons'; import LoginVisible from '@/components/common/LoginVisible'; -import HTMLViewer from '@/components/editor/HTMLViewer'; +import HTMLViewer from '@/components/form/html/HTMLViewer'; import PageLayout from '@/components/layout/pageLayout/PageLayout'; import { Link } from '@/i18n/routing'; import { Guide } from '@/types/academics'; diff --git a/app/[locale]/academics/helper/ScholarshipDetail.tsx b/app/[locale]/academics/components/scholarship/ScholarshipDetail.tsx similarity index 96% rename from app/[locale]/academics/helper/ScholarshipDetail.tsx rename to app/[locale]/academics/components/scholarship/ScholarshipDetail.tsx index 204070087..295dd7ec3 100644 --- a/app/[locale]/academics/helper/ScholarshipDetail.tsx +++ b/app/[locale]/academics/components/scholarship/ScholarshipDetail.tsx @@ -3,7 +3,7 @@ import { deleteScholarshipAction } from '@/actions/academics'; import { DeleteButton, EditButton } from '@/components/common/Buttons'; import LoginVisible from '@/components/common/LoginVisible'; -import HTMLViewer from '@/components/editor/HTMLViewer'; +import HTMLViewer from '@/components/form/html/HTMLViewer'; import PageLayout from '@/components/layout/pageLayout/PageLayout'; import { Scholarship, StudentType } from '@/types/academics'; import { errorToStr } from '@/utils/error'; diff --git a/app/[locale]/academics/components/scholarship/ScholarshipEditor.tsx b/app/[locale]/academics/components/scholarship/ScholarshipEditor.tsx new file mode 100644 index 000000000..231ce9d4b --- /dev/null +++ b/app/[locale]/academics/components/scholarship/ScholarshipEditor.tsx @@ -0,0 +1,80 @@ +'use client'; + +import { useState } from 'react'; +import { FormProvider, useForm } from 'react-hook-form'; + +import Fieldset from '@/components/form/Fieldset'; +import LanguagePicker from '@/components/form/LanguagePicker'; +import Form from '@/components/form/Form'; +import HTMLEditor from '@/components/form/html/HTMLEditor'; +import { useRouter } from '@/i18n/routing'; +import { Language } from '@/types/language'; +import { errorToStr } from '@/utils/error'; +import { handleServerAction } from '@/utils/serverActionError'; +import { errorToast, successToast } from '@/utils/toast'; + +export type ScholarshipFormData = { + koName: string; + koDescription: string; + enName: string; + enDescription: string; +}; + +type Props = { + defaultValues?: ScholarshipFormData; + cancelPath: string; + onSubmit: (data: ScholarshipFormData) => Promise; +}; + +export default function ScholarshipEditor({ + defaultValues, + cancelPath, + onSubmit: _onSubmit, +}: Props) { + const formMethods = useForm({ + defaultValues: defaultValues ?? { + koName: '', + koDescription: '', + enName: '', + enDescription: '', + }, + }); + const { handleSubmit } = formMethods; + const router = useRouter(); + const [language, setLanguage] = useState('ko'); + + const onSubmit = async (formData: ScholarshipFormData) => { + try { + handleServerAction(_onSubmit(formData)); + successToast('장학금을 수정했습니다.'); + } catch (e) { + errorToast(errorToStr(e)); + } + }; + + const onCancel = () => router.push(cancelPath); + + return ( + +
    + + {language === 'ko' && } + {language === 'en' && } + + +
    + ); +} + +const Editor = ({ language }: { language: Language }) => { + return ( + <> + + + + + + + + ); +}; diff --git a/app/[locale]/academics/components/scholarship/ScholarshipGuideEditor.tsx b/app/[locale]/academics/components/scholarship/ScholarshipGuideEditor.tsx new file mode 100644 index 000000000..b50cafd02 --- /dev/null +++ b/app/[locale]/academics/components/scholarship/ScholarshipGuideEditor.tsx @@ -0,0 +1,52 @@ +'use client'; + +import { FormProvider, useForm } from 'react-hook-form'; + +import Fieldset from '@/components/form/Fieldset'; +import Form from '@/components/form/Form'; +import HTMLEditor from '@/components/form/html/HTMLEditor'; +import { useRouter } from '@/i18n/routing'; +import { errorToStr } from '@/utils/error'; +import { handleServerAction } from '@/utils/serverActionError'; +import { errorToast } from '@/utils/toast'; + +export type ScholarshipGuideFormData = { + description: string; +}; + +type Props = { + description: string; + cancelPath: string; + onSubmit: (data: ScholarshipGuideFormData) => Promise; +}; + +export default function ScholarshipGuideEditor({ + description, + cancelPath, + onSubmit: _onSubmit, +}: Props) { + const formMethods = useForm({ defaultValues: { description } }); + const { handleSubmit } = formMethods; + const router = useRouter(); + + const onSubmit = async (formData: ScholarshipGuideFormData) => { + try { + handleServerAction(await _onSubmit(formData)); + } catch (e) { + errorToast(errorToStr(e)); + } + }; + + const onCancel = () => router.replace(cancelPath); + + return ( + +
    + + + + + +
    + ); +} diff --git a/app/[locale]/academics/helper/ScholarshipPreview.tsx b/app/[locale]/academics/components/scholarship/ScholarshipPreview.tsx similarity index 97% rename from app/[locale]/academics/helper/ScholarshipPreview.tsx rename to app/[locale]/academics/components/scholarship/ScholarshipPreview.tsx index 194726e56..7a4352cd8 100644 --- a/app/[locale]/academics/helper/ScholarshipPreview.tsx +++ b/app/[locale]/academics/components/scholarship/ScholarshipPreview.tsx @@ -2,7 +2,7 @@ import { BlackButton } from '@/components/common/Buttons'; import LoginVisible from '@/components/common/LoginVisible'; -import HTMLViewer from '@/components/editor/HTMLViewer'; +import HTMLViewer from '@/components/form/html/HTMLViewer'; import PageLayout from '@/components/layout/pageLayout/PageLayout'; import { Link } from '@/i18n/routing'; import { StudentType } from '@/types/academics'; diff --git a/app/[locale]/academics/helper/timeline/Timeline.tsx b/app/[locale]/academics/components/timeline/Timeline.tsx similarity index 100% rename from app/[locale]/academics/helper/timeline/Timeline.tsx rename to app/[locale]/academics/components/timeline/Timeline.tsx diff --git a/app/[locale]/academics/components/timeline/TimelineEditor.tsx b/app/[locale]/academics/components/timeline/TimelineEditor.tsx new file mode 100644 index 000000000..88aa114dd --- /dev/null +++ b/app/[locale]/academics/components/timeline/TimelineEditor.tsx @@ -0,0 +1,85 @@ +'use client'; + +import { useRouter } from 'next/router'; +import { FormProvider, useForm } from 'react-hook-form'; + +import Fieldset from '@/components/form/Fieldset'; +import { isUploadedFile, PostEditorFile } from '@/components/form/types'; +import Form from '@/components/form/Form'; +import HTMLEditor from '@/components/form/html/HTMLEditor'; +import { errorToStr } from '@/utils/error'; +import { contentToFormData, getAttachmentDeleteIds } from '@/utils/formData'; +import { handleServerAction } from '@/utils/serverActionError'; +import { errorToast, successToast } from '@/utils/toast'; + +export type TimelineFormData = { year: number; description: string; file: PostEditorFile[] }; + +interface Props { + defaultValues?: TimelineFormData; + onSubmit: (data: FormData) => Promise; + cancelPath: string; +} + +export default function TimelineEditor({ defaultValues, onSubmit: _onSubmit, cancelPath }: Props) { + const formMethods = useForm({ + defaultValues: defaultValues ?? { + year: new Date().getFullYear() + 1, + description: '', + file: [], + }, + }); + const { handleSubmit } = formMethods; + + const router = useRouter(); + const onCancel = () => router.push(cancelPath); + + const onSubmit = async (requestObject: TimelineFormData) => { + const isEdit = defaultValues !== undefined; + + const formData = isEdit + ? contentToFormData('EDIT', { + requestObject: { + ...requestObject, + deleteIds: getAttachmentDeleteIds( + requestObject.file, + defaultValues.file.filter(isUploadedFile).map(({ file: { id } }) => id), + ), + }, + attachments: requestObject.file, + }) + : contentToFormData('CREATE', { + // TODO: name 제거 + requestObject: { ...requestObject, name: '' }, + attachments: requestObject.file, + }); + + try { + handleServerAction(await _onSubmit(formData)); + successToast('저장되었습니다.'); + } catch (e) { + errorToast(errorToStr(e)); + } + }; + + return ( + +
    +
    + +
    + + + + + + + + +
    + ); +} diff --git a/app/[locale]/academics/helper/timeline/TimelineViewer.tsx b/app/[locale]/academics/components/timeline/TimelineViewer.tsx similarity index 98% rename from app/[locale]/academics/helper/timeline/TimelineViewer.tsx rename to app/[locale]/academics/components/timeline/TimelineViewer.tsx index 8dc9c3229..6d360653c 100644 --- a/app/[locale]/academics/helper/timeline/TimelineViewer.tsx +++ b/app/[locale]/academics/components/timeline/TimelineViewer.tsx @@ -4,7 +4,7 @@ import { useReducer, useState } from 'react'; import { DeleteButton, EditButton } from '@/components/common/Buttons'; import LoginVisible from '@/components/common/LoginVisible'; -import HTMLViewer from '@/components/editor/HTMLViewer'; +import HTMLViewer from '@/components/form/html/HTMLViewer'; import { Link, usePathname } from '@/i18n/routing'; import { errorToStr } from '@/utils/error'; import { refreshPage } from '@/utils/refreshPage'; diff --git a/app/[locale]/academics/graduate/course-changes/create/page.tsx b/app/[locale]/academics/graduate/course-changes/create/page.tsx index e29d0f3b9..54c424555 100644 --- a/app/[locale]/academics/graduate/course-changes/create/page.tsx +++ b/app/[locale]/academics/graduate/course-changes/create/page.tsx @@ -1,21 +1,23 @@ -'use client'; +import { redirectKo } from 'next/dist/client/components/redirect'; -import { postCourseChangesAction } from '@/actions/academics'; +import { postAcademicsByPostType } from '@/apis/v1/academics/[studentType]/[postType]'; +import TimelineEditor from '@/app/[locale]/academics/components/timeline/TimelineEditor'; import PageLayout from '@/components/layout/pageLayout/PageLayout'; import { getPath } from '@/utils/page'; -import { graduateCourseChanges } from '@/utils/segmentNode'; +import { undergraduateCourseChanges } from '@/utils/segmentNode'; -import TimelineEditor from '../../../helper/timeline/TimelineEditor'; +const courseChangesPath = getPath(undergraduateCourseChanges); -const courseChangesPath = getPath(graduateCourseChanges); +export default function Page() { + const onSubmit = async (formData: FormData) => { + 'use server'; + await postAcademicsByPostType('graduate', 'course-changes', formData); + redirectKo(courseChangesPath); + }; -export default function GraduateCourseChangesCreatePage() { return ( - postCourseChangesAction('graduate', data)} - fallbackPathname={courseChangesPath} - /> + ); } diff --git a/app/[locale]/academics/graduate/course-changes/edit/page.tsx b/app/[locale]/academics/graduate/course-changes/edit/page.tsx index a375c227b..ac03abed9 100644 --- a/app/[locale]/academics/graduate/course-changes/edit/page.tsx +++ b/app/[locale]/academics/graduate/course-changes/edit/page.tsx @@ -1,16 +1,20 @@ -import { getCourseChanges } from '@/apis/v1/academics/[type]/course-changes'; +import { revalidateTag } from 'next/cache'; + +import { getCourseChanges } from '@/apis/v1/academics/[studentType]/course-changes'; +import { putCourseChanges } from '@/apis/v1/academics/[studentType]/course-changes/[year]'; +import TimelineEditor, { + TimelineFormData, +} from '@/app/[locale]/academics/components/timeline/TimelineEditor'; +import PageLayout from '@/components/layout/pageLayout/PageLayout'; +import { FETCH_TAG_COURSE_CHANGES } from '@/constants/network'; +import { redirectKo } from '@/i18n/routing'; import { getPath } from '@/utils/page'; import { graduateCourseChanges } from '@/utils/segmentNode'; - -import CourseChangesEditPageContent from '../../../helper/course-changes/CourseChangeEditPageContent'; +import { decodeFormDataFileName } from '@/utils/string'; const courseChangePath = getPath(graduateCourseChanges); -export default async function GraduateCourseChangesEditPage({ - searchParams, -}: { - searchParams: { year: string }; -}) { +export default async function Page({ searchParams }: { searchParams: { year: string } }) { const data = await getCourseChanges('graduate'); const year = Number(searchParams.year); const selected = data.find((x) => x.year === year); @@ -19,11 +23,27 @@ export default async function GraduateCourseChangesEditPage({ return
    해당 연도 내용이 존재하지 않습니다.
    ; } + const onSubmit = async (formData: FormData) => { + 'use server'; + decodeFormDataFileName(formData, 'newAttachments'); + await putCourseChanges('graduate', year, formData); + revalidateTag(FETCH_TAG_COURSE_CHANGES); + redirectKo(courseChangePath); + }; + + const defaultValues: TimelineFormData = { + year: selected.year, + description: selected.description, + file: selected.attachments.map((file) => ({ type: 'UPLOADED_FILE', file })), + }; + return ( - + + + ); } diff --git a/app/[locale]/academics/graduate/course-changes/page.tsx b/app/[locale]/academics/graduate/course-changes/page.tsx index cc4e18fad..853a9c031 100644 --- a/app/[locale]/academics/graduate/course-changes/page.tsx +++ b/app/[locale]/academics/graduate/course-changes/page.tsx @@ -1,21 +1,33 @@ -import { Metadata } from 'next'; +import { revalidateTag } from 'next/cache'; -import { getCourseChanges } from '@/apis/v1/academics/[type]/course-changes'; +import { getCourseChanges } from '@/apis/v1/academics/[studentType]/course-changes'; +import { deleteCourseChanges } from '@/apis/v1/academics/[studentType]/course-changes/[year]'; +import TimelineViewer from '@/app/[locale]/academics/components/timeline/TimelineViewer'; +import PageLayout from '@/components/layout/pageLayout/PageLayout'; +import { FETCH_TAG_COURSE_CHANGES } from '@/constants/network'; import { getMetadata } from '@/utils/metadata'; -import { graduateCourseChanges } from '@/utils/segmentNode'; +import { undergraduateCourseChanges } from '@/utils/segmentNode'; -import CourseChangesPageContent from '../../helper/course-changes/CourseChangesPageContent'; - -export async function generateMetadata({ - params: { locale }, -}: { - params: { locale: string }; -}): Promise { - return await getMetadata({ locale, node: graduateCourseChanges }); +export async function generateMetadata({ params: { locale } }: { params: { locale: string } }) { + return await getMetadata({ locale, node: undergraduateCourseChanges }); } -export default async function GraduateCourseChangesPage() { +export default async function UndergraduateCourseChangesPage() { const changes = await getCourseChanges('graduate'); - return ; + const onDelete = async (year: number) => { + 'use server'; + await deleteCourseChanges('undergraduate', year); + revalidateTag(FETCH_TAG_COURSE_CHANGES); + }; + + return ( + + + + ); } diff --git a/app/[locale]/academics/graduate/courses/GraduateCoursePageContent.tsx b/app/[locale]/academics/graduate/courses/GraduateCoursePageContent.tsx index bbafaaf1d..6810ae4c6 100644 --- a/app/[locale]/academics/graduate/courses/GraduateCoursePageContent.tsx +++ b/app/[locale]/academics/graduate/courses/GraduateCoursePageContent.tsx @@ -7,11 +7,11 @@ import { Course } from '@/types/academics'; import { Language } from '@/types/language'; import useResponsive from '@/utils/hooks/useResponsive'; -import AddCourseButton from '../../helper/courses/AddCourseButton'; -import CourseCards from '../../helper/courses/CourseCards'; -import CourseList from '../../helper/courses/CourseList'; -import CourseToolbar from '../../helper/courses/CourseToolbar'; -import useCourseToolbar from '../../helper/courses/useCourseToolbar'; +import AddCourseButton from '../../components/courses/AddCourseButton'; +import CourseCards from '../../components/courses/CourseCards'; +import CourseList from '../../components/courses/CourseList'; +import CourseToolbar from '../../components/courses/CourseToolbar'; +import useCourseToolbar from '../../components/courses/useCourseToolbar'; interface CoursePageContentProps { courses: Course[]; diff --git a/app/[locale]/academics/graduate/guide/edit/page.tsx b/app/[locale]/academics/graduate/guide/edit/page.tsx index 417dc1942..404db9bd6 100644 --- a/app/[locale]/academics/graduate/guide/edit/page.tsx +++ b/app/[locale]/academics/graduate/guide/edit/page.tsx @@ -1,9 +1,31 @@ -import { getAcademicsGuide } from '@/apis/v1/academics/[type]/guide'; +import { revalidateTag } from 'next/cache'; -import GuideEditPageContent from '../../../helper/guide/GuideEditPageContent'; +import { getAcademicsGuide, putAcademicsGuide } from '@/apis/v1/academics/[studentType]/guide'; +import GuideEditBridge from '@/app/[locale]/academics/components/guide/GuideEditorBridge'; +import PageLayout from '@/components/layout/pageLayout/PageLayout'; +import { FETCH_TAG_GUIDE } from '@/constants/network'; +import { redirectKo } from '@/i18n/routing'; +import { getPath } from '@/utils/page'; +import { graduateGuide } from '@/utils/segmentNode'; +import { decodeFormDataFileName } from '@/utils/string'; -export default async function GraduateGuideEditPage() { +const path = getPath(graduateGuide); + +export default async function Page() { const data = await getAcademicsGuide('graduate'); + console.log(data); + + const serverAction = async (formData: FormData) => { + 'use server'; + decodeFormDataFileName(formData, 'newAttachments'); + await putAcademicsGuide('graduate', formData); + revalidateTag(FETCH_TAG_GUIDE); + redirectKo(path); + }; - return ; + return ( + + + + ); } diff --git a/app/[locale]/academics/graduate/guide/page.tsx b/app/[locale]/academics/graduate/guide/page.tsx index 61524da7b..c0efc447c 100644 --- a/app/[locale]/academics/graduate/guide/page.tsx +++ b/app/[locale]/academics/graduate/guide/page.tsx @@ -1,9 +1,9 @@ -import { getAcademicsGuide } from '@/apis/v1/academics/[type]/guide'; +import { getAcademicsGuide } from '@/apis/v1/academics/[studentType]/guide'; import { getMetadata } from '@/utils/metadata'; import { getPath } from '@/utils/page'; import { graduateGuide } from '@/utils/segmentNode'; -import GuidePageContent from '../../helper/guide/GuidePageContent'; +import GuidePageContent from '../../components/guide/GuidePageContent'; export async function generateMetadata({ params: { locale } }: { params: { locale: string } }) { return await getMetadata({ locale, node: graduateGuide }); diff --git a/app/[locale]/academics/graduate/scholarship/[id]/edit/page.tsx b/app/[locale]/academics/graduate/scholarship/[id]/edit/page.tsx index 1d430136c..774ac8145 100644 --- a/app/[locale]/academics/graduate/scholarship/[id]/edit/page.tsx +++ b/app/[locale]/academics/graduate/scholarship/[id]/edit/page.tsx @@ -1,7 +1,18 @@ +import { revalidateTag } from 'next/cache'; + +import { putScholarship } from '@/apis/v2/academics/scholarship'; import { getScholarship } from '@/apis/v2/academics/scholarship/[id]'; +import ScholarshipEditor, { + ScholarshipFormData, +} from '@/app/[locale]/academics/components/scholarship/ScholarshipEditor'; import InvalidIDFallback from '@/components/common/InvalidIDFallback'; +import PageLayout from '@/components/layout/pageLayout/PageLayout'; +import { FETCH_TAG_SCHOLARSHIP } from '@/constants/network'; +import { redirectKo } from '@/i18n/routing'; +import { getPath } from '@/utils/page'; +import { graduateScholarship } from '@/utils/segmentNode'; -import ScholarshipDetailEdit from '../../../../helper/ScholarshipDetailEdit'; +const path = getPath(graduateScholarship); export default async function UndergraduateScholarshipEditPage({ params, @@ -9,8 +20,34 @@ export default async function UndergraduateScholarshipEditPage({ params: { id: string }; }) { try { - const scholarship = await getScholarship(parseInt(params.id)); - return ; + const id = parseInt(params.id); + + const scholarship = await getScholarship(id); + + const onSubmit = async (content: ScholarshipFormData) => { + 'use server'; + await putScholarship(id, { + ko: { ...scholarship.ko, name: content.koName, description: content.koDescription }, + en: { ...scholarship.en, name: content.enName, description: content.enDescription }, + }); + revalidateTag(FETCH_TAG_SCHOLARSHIP); + redirectKo(path); + }; + + return ( + + + + ); } catch { return ; } diff --git a/app/[locale]/academics/graduate/scholarship/[id]/page.tsx b/app/[locale]/academics/graduate/scholarship/[id]/page.tsx index 6d9de7244..ea0a5a27d 100644 --- a/app/[locale]/academics/graduate/scholarship/[id]/page.tsx +++ b/app/[locale]/academics/graduate/scholarship/[id]/page.tsx @@ -4,7 +4,7 @@ import { Language } from '@/types/language'; import { getMetadata } from '@/utils/metadata'; import { graduateScholarship } from '@/utils/segmentNode'; -import ScholarshipDetail from '../../../helper/ScholarshipDetail'; +import ScholarshipDetail from '../../../components/scholarship/ScholarshipDetail'; interface ScholarshipDetailProps { params: { locale: Language; id: string }; diff --git a/app/[locale]/academics/graduate/scholarship/create/page.tsx b/app/[locale]/academics/graduate/scholarship/create/page.tsx index 41c3f1972..ac7782456 100644 --- a/app/[locale]/academics/graduate/scholarship/create/page.tsx +++ b/app/[locale]/academics/graduate/scholarship/create/page.tsx @@ -1,5 +1,28 @@ -import ScholarshipCreatePage from '../../../helper/ScholarshipCreatePage'; +import { revalidateTag } from 'next/cache'; + +import { postScholarship } from '@/apis/v2/academics/[type]/scholarship'; +import ScholarshipEditor, { + ScholarshipFormData, +} from '@/app/[locale]/academics/components/scholarship/ScholarshipEditor'; +import PageLayout from '@/components/layout/pageLayout/PageLayout'; +import { FETCH_TAG_SCHOLARSHIP } from '@/constants/network'; +import { redirectKo } from '@/i18n/routing'; +import { getPath } from '@/utils/page'; +import { graduateScholarship } from '@/utils/segmentNode'; + +const path = getPath(graduateScholarship); export default function GraduateScholarshipCreatePage() { - return ; + const onSubmit = async (content: ScholarshipFormData) => { + 'use server'; + await postScholarship('graduate', content); + revalidateTag(FETCH_TAG_SCHOLARSHIP); + redirectKo(path); + }; + + return ( + + + + ); } diff --git a/app/[locale]/academics/graduate/scholarship/edit/page.tsx b/app/[locale]/academics/graduate/scholarship/edit/page.tsx index 1e245c3e4..1225c0f95 100644 --- a/app/[locale]/academics/graduate/scholarship/edit/page.tsx +++ b/app/[locale]/academics/graduate/scholarship/edit/page.tsx @@ -1,9 +1,34 @@ +import { revalidateTag } from 'next/cache'; + import { getScholarshipList } from '@/apis/v1/academics/scholarship'; +import { putScholarshipGuide } from '@/apis/v2/academics/[type]/scholarship'; +import PageLayout from '@/components/layout/pageLayout/PageLayout'; +import { FETCH_TAG_SCHOLARSHIP } from '@/constants/network'; +import { redirectKo } from '@/i18n/routing'; +import { getPath } from '@/utils/page'; +import { graduateScholarship } from '@/utils/segmentNode'; +import { successToast } from '@/utils/toast'; + +import ScholarshipGuideEditor, { + ScholarshipGuideFormData, +} from '../../../components/scholarship/ScholarshipGuideEditor'; -import ScholarshipGuideEditPageContent from '../../../helper/ScholarshipGuideEditPageContent'; +const path = getPath(graduateScholarship); -export default async function GraduateScholarshipListEditPage() { +export default async function UndergraduateScholarshipListEditPage() { const { description } = await getScholarshipList('graduate'); - return ; + const onSubmit = async ({ description }: ScholarshipGuideFormData) => { + 'use server'; + await putScholarshipGuide('graduate', description); + revalidateTag(FETCH_TAG_SCHOLARSHIP); + redirectKo(path); + successToast('장학 제도 안내를 수정했습니다.'); + }; + + return ( + + + + ); } diff --git a/app/[locale]/academics/graduate/scholarship/page.tsx b/app/[locale]/academics/graduate/scholarship/page.tsx index 020818429..1d69be17a 100644 --- a/app/[locale]/academics/graduate/scholarship/page.tsx +++ b/app/[locale]/academics/graduate/scholarship/page.tsx @@ -2,7 +2,7 @@ import { getScholarshipList } from '@/apis/v1/academics/scholarship'; import { getMetadata } from '@/utils/metadata'; import { graduateScholarship } from '@/utils/segmentNode'; -import ScholarshipPreview from '../../helper/ScholarshipPreview'; +import ScholarshipPreview from '../../components/scholarship/ScholarshipPreview'; export async function generateMetadata({ params: { locale } }: { params: { locale: string } }) { return await getMetadata({ locale, node: graduateScholarship }); diff --git a/app/[locale]/academics/helper/ScholarshipCreatePage.tsx b/app/[locale]/academics/helper/ScholarshipCreatePage.tsx deleted file mode 100644 index abc1fed8c..000000000 --- a/app/[locale]/academics/helper/ScholarshipCreatePage.tsx +++ /dev/null @@ -1,53 +0,0 @@ -'use client'; - -import { postScholarshipAction } from '@/actions/academics'; -import BasicEditor, { BasicEditorContent } from '@/components/editor/BasicEditor'; -import PageLayout from '@/components/layout/pageLayout/PageLayout'; -import { useRouter } from '@/i18n/routing'; -import { StudentType } from '@/types/academics'; -import { errorToStr } from '@/utils/error'; -import { validateBasicForm } from '@/utils/formValidation'; -import { getPath } from '@/utils/page'; -import { graduateScholarship, undergraduateScholarship } from '@/utils/segmentNode'; -import { handleServerAction } from '@/utils/serverActionError'; -import { errorToast, successToast } from '@/utils/toast'; - -const undergraduate = getPath(undergraduateScholarship); -const graduate = getPath(graduateScholarship); - -export default function ScholarshipCreatePage({ type }: { type: StudentType }) { - const router = useRouter(); - - const handleCancel = () => router.push(type === 'undergraduate' ? undergraduate : graduate); - - const handleSubmit = async (content: BasicEditorContent) => { - validateBasicForm(content, { titleRequired: true }); - - try { - handleServerAction( - await postScholarshipAction(type, { - koName: content.title.ko, - koDescription: content.description.ko, - enName: content.title.en, - enDescription: content.description.en, - }), - ); - successToast('장학금을 추가했습니다.'); - } catch (e) { - errorToast(errorToStr(e)); - } - }; - - return ( - - - - ); -} diff --git a/app/[locale]/academics/helper/ScholarshipDetailEdit.tsx b/app/[locale]/academics/helper/ScholarshipDetailEdit.tsx deleted file mode 100644 index d8821981b..000000000 --- a/app/[locale]/academics/helper/ScholarshipDetailEdit.tsx +++ /dev/null @@ -1,68 +0,0 @@ -'use client'; - -import { putScholarshipAction } from '@/actions/academics'; -import BasicEditor, { BasicEditorContent } from '@/components/editor/BasicEditor'; -import PageLayout from '@/components/layout/pageLayout/PageLayout'; -import { useRouter } from '@/i18n/routing'; -import { Scholarship, StudentType } from '@/types/academics'; -import { WithLanguage } from '@/types/language'; -import { errorToStr } from '@/utils/error'; -import { validateBasicForm } from '@/utils/formValidation'; -import { useTypedLocale } from '@/utils/hooks/useTypedLocale'; -import { getPath } from '@/utils/page'; -import { academics } from '@/utils/segmentNode'; -import { handleServerAction } from '@/utils/serverActionError'; -import { errorToast, successToast } from '@/utils/toast'; - -const academicsPath = getPath(academics); - -export default function ScholarshipDetailEdit({ - type, - scholarship, -}: { - type: StudentType; - scholarship: WithLanguage; -}) { - const language = useTypedLocale(); - const router = useRouter(); - - const handleCancel = () => router.replace(`${academicsPath}/${type}/scholarship`); - - const handleSubmit = async (content: BasicEditorContent) => { - validateBasicForm(content, { titleRequired: true }); - - const { title: name, description } = content; - const newData = { - ko: { ...scholarship.ko, name: name.ko, description: description.ko }, - en: { ...scholarship.en, name: name.en, description: description.en }, - }; - - try { - handleServerAction(await putScholarshipAction(type, scholarship[language].id, newData)); - successToast('장학금을 수정했습니다.'); - } catch (e) { - errorToast(errorToStr(e)); - } - }; - - return ( - - - - ); -} diff --git a/app/[locale]/academics/helper/ScholarshipGuideEditPageContent.tsx b/app/[locale]/academics/helper/ScholarshipGuideEditPageContent.tsx deleted file mode 100644 index 10b5a0cef..000000000 --- a/app/[locale]/academics/helper/ScholarshipGuideEditPageContent.tsx +++ /dev/null @@ -1,57 +0,0 @@ -'use client'; - -import { putScholarshipGuideAction } from '@/actions/academics'; -import BasicEditor, { BasicEditorContent } from '@/components/editor/BasicEditor'; -import PageLayout from '@/components/layout/pageLayout/PageLayout'; -import { useRouter } from '@/i18n/routing'; -import { StudentType } from '@/types/academics'; -import { errorToStr } from '@/utils/error'; -import { getPath } from '@/utils/page'; -import { academics } from '@/utils/segmentNode'; -import { handleServerAction } from '@/utils/serverActionError'; -import { errorToast, successToast } from '@/utils/toast'; - -const academicsPath = getPath(academics); - -export default function ScholarshipGuideEditPageContent({ - description, - type, -}: { - description: string; - type: StudentType; -}) { - const router = useRouter(); - - const handleCancel = () => router.replace(`${academicsPath}/${type}/scholarship`); - - const handleSubmit = async (content: BasicEditorContent) => { - if (!content.description.ko) { - throw new Error('내용을 입력해주세요'); - } - - try { - handleServerAction(await putScholarshipGuideAction(type, content.description.ko)); - successToast('장학 제도 안내를 수정했습니다.'); - } catch (e) { - errorToast(errorToStr(e)); - } - }; - - return ( - - - - ); -} diff --git a/app/[locale]/academics/helper/course-changes/CourseChangeEditPageContent.tsx b/app/[locale]/academics/helper/course-changes/CourseChangeEditPageContent.tsx deleted file mode 100644 index 330a319db..000000000 --- a/app/[locale]/academics/helper/course-changes/CourseChangeEditPageContent.tsx +++ /dev/null @@ -1,30 +0,0 @@ -'use client'; - -import { putCourseChangesAction } from '@/actions/academics'; -import PageLayout from '@/components/layout/pageLayout/PageLayout'; -import { CourseChange, StudentType } from '@/types/academics'; - -import TimelineEditor from '../timeline/TimelineEditor'; - -export default function CourseChangesEditPageContent({ - type, - initContent, - courseChangePath, -}: { - type: StudentType; - initContent: CourseChange; - courseChangePath: string; -}) { - return ( - - putCourseChangesAction(type, data)} - fallbackPathname={courseChangePath} - initialContent={initContent} - /> - - ); -} diff --git a/app/[locale]/academics/helper/course-changes/CourseChangesPageContent.tsx b/app/[locale]/academics/helper/course-changes/CourseChangesPageContent.tsx deleted file mode 100644 index bddd10b6e..000000000 --- a/app/[locale]/academics/helper/course-changes/CourseChangesPageContent.tsx +++ /dev/null @@ -1,28 +0,0 @@ -'use client'; - -import { deleteCourseChangesAction } from '@/actions/academics'; -import PageLayout from '@/components/layout/pageLayout/PageLayout'; -import { CourseChange, StudentType } from '@/types/academics'; - -import TimelineViewer from '../timeline/TimelineViewer'; - -export default function CourseChangesPageContent({ - type, - changes, -}: { - type: StudentType; - changes: CourseChange[]; -}) { - return ( - - deleteCourseChangesAction(type, year)} - /> - - ); -} diff --git a/app/[locale]/academics/helper/courses/AddCourseModal.tsx b/app/[locale]/academics/helper/courses/AddCourseModal.tsx deleted file mode 100644 index 8131570eb..000000000 --- a/app/[locale]/academics/helper/courses/AddCourseModal.tsx +++ /dev/null @@ -1,239 +0,0 @@ -import { postCourseAction } from '@/actions/academics'; -import Dropdown from '@/components/common/form/Dropdown'; -import BasicTextInput from '@/components/editor/common/BasicTextInput'; -import Fieldset from '@/components/editor/common/Fieldset'; -import ModalFrame from '@/components/modal/ModalFrame'; -import { CLASSIFICATION, Classification, Course, GRADE, StudentType } from '@/types/academics'; -import { getKeys } from '@/types/object'; -import { errorToStr } from '@/utils/error'; -import { validateCourseForm } from '@/utils/formValidation'; -import { handleServerAction } from '@/utils/serverActionError'; -import { errorToast, successToast } from '@/utils/toast'; - -import useCourseEditor from './useCourseEditor'; - -const getInitCourse = (type: StudentType): Course => ({ - code: '', - credit: 3, - grade: type === 'graduate' ? 0 : 1, - studentType: type, - ko: { name: '', description: '', classification: '전공필수' }, - en: { name: '', description: '', classification: 'RM' }, -}); - -export default function AddCourseModal({ - onClose, - studentType, -}: { - onClose: () => void; - studentType: StudentType; -}) { - const { content, setContentByKey, setLanguageContent, setClassification } = useCourseEditor( - getInitCourse(studentType), - ); - - const handleSubmit = async () => { - try { - validateCourseForm(content); - handleServerAction(await postCourseAction(content)); - successToast('새 교과목을 추가했습니다.'); - onClose(); - } catch (e) { - errorToast(errorToStr(e)); - } - }; - - return ( - -
    -

    교과목 추가

    -
    - setLanguageContent('name', value, 'ko')} - /> - setLanguageContent('description', value, 'ko')} - /> -
    - - - - -
    -
    - * 교과목 번호는 추후 수정할 수 없습니다. -
    - setLanguageContent('name', value, 'en')} - english - /> - setLanguageContent('description', value, 'en')} - english - /> -
    -
    -
    -
    -
    - ); -} - -function Button({ text, onClick }: { text: string; onClick: () => void }) { - return ( - - ); -} - -function NameFieldset({ - name, - onChange, - english = false, -}: { - name: string; - onChange: (value: string) => void; - english?: boolean; -}) { - return ( -
    - -
    - ); -} - -function DescriptionFieldset({ - description, - onChange, - english = false, -}: { - description: string; - onChange: (value: string) => void; - english?: boolean; -}) { - return ( -
    -