From 08a3a55fe6685e360f7db9ace8baf08c7b51b099 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=B1=EC=97=B4?= Date: Sun, 9 Mar 2025 02:43:22 +0900 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20=ED=95=99=EC=83=9D=ED=9A=8C?= =?UTF-8?q?=EC=B9=99=20=ED=8E=B8=EC=A7=91=20=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?(#325)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- actions/council.ts | 28 +++++- apis/v2/council/rule.ts | 9 +- app/[locale]/community/council/intro/page.tsx | 2 +- .../report/components/CouncilReportEditor.tsx | 2 + .../community/council/rules/edit/client.tsx | 91 +++++++++++++++++++ .../community/council/rules/edit/page.tsx | 13 +++ .../council/{bylaws => rules}/page.tsx | 15 ++- components/form/File.tsx | 24 ++--- components/form/Text.tsx | 14 ++- constants/network.ts | 1 + constants/segmentNode.ts | 2 +- utils/formData.ts | 8 ++ 12 files changed, 186 insertions(+), 23 deletions(-) create mode 100644 app/[locale]/community/council/rules/edit/client.tsx create mode 100644 app/[locale]/community/council/rules/edit/page.tsx rename app/[locale]/community/council/{bylaws => rules}/page.tsx (70%) diff --git a/actions/council.ts b/actions/council.ts index bd5cb529..53567466 100644 --- a/actions/council.ts +++ b/actions/council.ts @@ -10,12 +10,19 @@ import { } from '@/apis/v2/council/meeting-minute'; import { postCouncilReport } from '@/apis/v2/council/report'; import { deleteCouncilReport, putCouncilReport } from '@/apis/v2/council/report/[id]'; +import { putCouncilRules } from '@/apis/v2/council/rule'; import { FETCH_TAG_COUNCIL_INTRO, FETCH_TAG_COUNCIL_MINUTE, FETCH_TAG_COUNCIL_REPORT, + FETCH_TAG_COUNCIL_RULES, } from '@/constants/network'; -import { councilIntro, councilMinute, councilReportList } from '@/constants/segmentNode'; +import { + councilBylaws as councilRules, + councilIntro, + councilMinute, + councilReportList, +} from '@/constants/segmentNode'; import { redirectKo } from '@/i18n/routing'; import { getPath } from '@/utils/page'; import { decodeFormDataFileName } from '@/utils/string'; @@ -84,3 +91,22 @@ export const deleteCouncilReportAction = async (id: number) => { } redirectKo(councilReportPath); }; + +/** 학생회칙 */ + +const councilRulesPath = getPath(councilRules); + +export const putCouncilRulesAction = withErrorHandler( + async (bylawFormData?: FormData, constitutionFormData?: FormData) => { + if (bylawFormData) decodeFormDataFileName(bylawFormData, 'newAttachments'); + if (constitutionFormData) decodeFormDataFileName(constitutionFormData, 'newAttachments'); + + await Promise.all([ + bylawFormData && putCouncilRules('bylaw', bylawFormData), + constitutionFormData && putCouncilRules('constitution', constitutionFormData), + ]); + + revalidateTag(FETCH_TAG_COUNCIL_RULES); + redirectKo(councilRulesPath); + }, +); diff --git a/apis/v2/council/rule.ts b/apis/v2/council/rule.ts index 6682967e..37838f4e 100644 --- a/apis/v2/council/rule.ts +++ b/apis/v2/council/rule.ts @@ -1,9 +1,12 @@ -import { getRequest } from '@/apis'; +import { getRequest, putRequest } from '@/apis'; import { Attachment } from '@/apis/types/attachment'; -type Response = { +export type CouncilRules = { constitution: { type: string; attachments: Attachment[] }; bylaw: { type: string; attachments: Attachment[] }; }; -export const getCouncilRule = () => getRequest('/v2/council/rule', undefined); +export const getCouncilRules = () => getRequest('/v2/council/rule', undefined); + +export const putCouncilRules = (type: keyof CouncilRules, body: FormData) => + putRequest(`/v2/council/rule/${type}`, { body, jsessionID: true }); diff --git a/app/[locale]/community/council/intro/page.tsx b/app/[locale]/community/council/intro/page.tsx index 27ac6e2c..7224631e 100644 --- a/app/[locale]/community/council/intro/page.tsx +++ b/app/[locale]/community/council/intro/page.tsx @@ -33,7 +33,7 @@ export default async function CouncilIntroPage() { return (
- +
diff --git a/app/[locale]/community/council/report/components/CouncilReportEditor.tsx b/app/[locale]/community/council/report/components/CouncilReportEditor.tsx index 46db4ec3..9526709e 100644 --- a/app/[locale]/community/council/report/components/CouncilReportEditor.tsx +++ b/app/[locale]/community/council/report/components/CouncilReportEditor.tsx @@ -50,6 +50,8 @@ export default function CouncilReportEditor({ onCancel, onSubmit, defaultValues maxWidth="w-[39px]" placeholder="39" options={{ required: true }} + type="number" + className="[appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none" />{' '} 대 학생회{' '} ({ + defaultValues: { + constitutionAttachments: attachmentsToEditorFiles(constitutionAttachments), + bylawAttachments: attachmentsToEditorFiles(bylawAttachments), + }, + }); + const router = useRouter(); + + const { + handleSubmit, + formState: { dirtyFields }, + } = methods; + + const onCancel = () => { + router.back(); + }; + + const onSubmit = handleSubmit(async (formData: FormData) => { + const bylawsFormData = dirtyFields.bylawAttachments + ? (() => { + const bylawsDeleteIds = getAttachmentDeleteIds( + formData.bylawAttachments, + bylawAttachments, + ); + return contentToFormData('EDIT', { + requestObject: { deleteIds: bylawsDeleteIds }, + attachments: formData.bylawAttachments, + }); + })() + : undefined; + + const constitutionFormData = dirtyFields.constitutionAttachments + ? (() => { + const constitutionDeleteIds = getAttachmentDeleteIds( + formData.constitutionAttachments, + constitutionAttachments, + ); + return contentToFormData('EDIT', { + requestObject: { deleteIds: constitutionDeleteIds }, + attachments: formData.constitutionAttachments, + }); + })() + : undefined; + + await putCouncilRulesAction(bylawsFormData, constitutionFormData); + }); + + return ( + +
+ + + + + + + + + + +
+ ); +} diff --git a/app/[locale]/community/council/rules/edit/page.tsx b/app/[locale]/community/council/rules/edit/page.tsx new file mode 100644 index 00000000..fc3a611a --- /dev/null +++ b/app/[locale]/community/council/rules/edit/page.tsx @@ -0,0 +1,13 @@ +import { getCouncilRules } from '@/apis/v2/council/rule'; +import CouncilByLawsEditClientPage from '@/app/[locale]/community/council/rules/edit/client'; +import PageLayout from '@/components/layout/pageLayout/PageLayout'; + +export default async function CouncilBylawsEditPage() { + const councilRules = await getCouncilRules(); + + return ( + + + + ); +} diff --git a/app/[locale]/community/council/bylaws/page.tsx b/app/[locale]/community/council/rules/page.tsx similarity index 70% rename from app/[locale]/community/council/bylaws/page.tsx rename to app/[locale]/community/council/rules/page.tsx index fe710ee5..80ec41de 100644 --- a/app/[locale]/community/council/bylaws/page.tsx +++ b/app/[locale]/community/council/rules/page.tsx @@ -1,11 +1,14 @@ export const dynamic = 'force-dynamic'; -import { getCouncilRule } from '@/apis/v2/council/rule'; +import { getCouncilRules } from '@/apis/v2/council/rule'; import CouncilAttachment from '@/app/[locale]/community/council/components/CouncilAttachments'; +import { EditButton } from '@/components/common/Buttons'; +import LoginVisible from '@/components/common/LoginVisible'; import PageLayout from '@/components/layout/pageLayout/PageLayout'; import { councilBylaws } from '@/constants/segmentNode'; import { Language } from '@/types/language'; import { getMetadata } from '@/utils/metadata'; +import { getPath } from '@/utils/page'; export async function generateMetadata(props: { params: Promise<{ locale: Language }> }) { const params = await props.params; @@ -14,11 +17,19 @@ export async function generateMetadata(props: { params: Promise<{ locale: Langua return await getMetadata({ locale, node: councilBylaws }); } +const editPath = `${getPath(councilBylaws)}/edit`; + export default async function CouncilIntroPage() { - const resp = await getCouncilRule(); + const resp = await getCouncilRules(); return ( + +
+ +
+
+

학생회칙

{resp.constitution.attachments.map((attachment) => ( diff --git a/components/form/File.tsx b/components/form/File.tsx index ab77f834..a01e9962 100644 --- a/components/form/File.tsx +++ b/components/form/File.tsx @@ -1,5 +1,5 @@ import { ChangeEventHandler, MouseEventHandler } from 'react'; -import { RegisterOptions, useFormContext, useWatch } from 'react-hook-form'; +import { FieldValues, RegisterOptions, useController, useFormContext } from 'react-hook-form'; import ClearIcon from '@/public/image/clear_icon.svg'; @@ -7,16 +7,18 @@ import { EditorFile, LocalFile } from '../../types/form'; interface FilePickerProps { name: string; - options?: RegisterOptions; + rules?: Omit< + RegisterOptions, + 'setValueAs' | 'disabled' | 'valueAsNumber' | 'valueAsDate' + >; multiple?: boolean; } -export default function FilePicker({ name, options, multiple = true }: FilePickerProps) { - const { register, setValue } = useFormContext(); - const files = useWatch({ name }) as EditorFile[]; - - register(name, options); - +export default function FilePicker({ name, rules, multiple = true }: FilePickerProps) { + const { control } = useFormContext(); + const { + field: { value: files, onChange }, + } = useController({ name, rules, control }); // 성능 확인 필요 const handleChange: ChangeEventHandler = (e) => { if (e.target.files === null) return; @@ -26,7 +28,7 @@ export default function FilePicker({ name, options, multiple = true }: FilePicke file, })); - setValue(name, [...files, ...newFiles]); + onChange([...files, ...newFiles]); // 같은 파일에 대해서 선택이 가능하도록 처리 // https://stackoverflow.com/a/12102992 @@ -36,7 +38,7 @@ export default function FilePicker({ name, options, multiple = true }: FilePicke const deleteFileAtIndex = (index: number) => { const nextFiles = [...files]; nextFiles.splice(index, 1); - setValue(name, nextFiles); + onChange(nextFiles); }; return ( @@ -47,7 +49,7 @@ export default function FilePicker({ name, options, multiple = true }: FilePicke self-start rounded-sm border-[1px] border-neutral-200 bg-neutral-50 `} > - {files.map((item, idx) => ( + {(files as EditorFile[]).map((item, idx) => ( diff --git a/constants/network.ts b/constants/network.ts index 7b13fe69..2631e18b 100644 --- a/constants/network.ts +++ b/constants/network.ts @@ -53,3 +53,4 @@ export const FETCH_TAG_INTERNATIONAL_UNDERGRADUATE = 'international-undergraduat export const FETCH_TAG_COUNCIL_INTRO = 'council-intro'; export const FETCH_TAG_COUNCIL_MINUTE = 'council-minute'; export const FETCH_TAG_COUNCIL_REPORT = 'council-report'; +export const FETCH_TAG_COUNCIL_RULES = 'council-rules'; diff --git a/constants/segmentNode.ts b/constants/segmentNode.ts index 88950356..73f3e6cf 100644 --- a/constants/segmentNode.ts +++ b/constants/segmentNode.ts @@ -152,7 +152,7 @@ export const councilMinute: SegmentNode = { export const councilBylaws: SegmentNode = { name: '학생회칙 및 세칙', - segment: 'bylaws', + segment: 'rules', isPage: true, parent: council, children: [], diff --git a/utils/formData.ts b/utils/formData.ts index 0eb00c17..e218c4bf 100644 --- a/utils/formData.ts +++ b/utils/formData.ts @@ -72,3 +72,11 @@ export function getEditorFile(attachment: Attachment | Attachment[]): EditorFile ? attachment.map((file) => ({ type: 'UPLOADED_FILE', file })) : { type: 'UPLOADED_FILE', file: attachment }; } + +export const attachmentToEditorFile = (attachment: Attachment): EditorFile => ({ + type: 'UPLOADED_FILE', + file: attachment, +}); + +export const attachmentsToEditorFiles = (attachments: Attachment[]): EditorFile[] => + attachments.map(attachmentToEditorFile);