Skip to content

Commit

Permalink
✨ 학생회칙 편집 페이지 (#325)
Browse files Browse the repository at this point in the history
  • Loading branch information
yeolyi authored Mar 8, 2025
1 parent 5d8e8fb commit 08a3a55
Show file tree
Hide file tree
Showing 12 changed files with 186 additions and 23 deletions.
28 changes: 27 additions & 1 deletion actions/council.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
},
);
9 changes: 6 additions & 3 deletions apis/v2/council/rule.ts
Original file line number Diff line number Diff line change
@@ -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<Response>('/v2/council/rule', undefined);
export const getCouncilRules = () => getRequest<CouncilRules>('/v2/council/rule', undefined);

export const putCouncilRules = (type: keyof CouncilRules, body: FormData) =>
putRequest<CouncilRules>(`/v2/council/rule/${type}`, { body, jsessionID: true });
2 changes: 1 addition & 1 deletion app/[locale]/community/council/intro/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export default async function CouncilIntroPage() {
return (
<PageLayout titleType="big" removePadding>
<div className="bg-neutral-100 px-5 pb-12 pt-7 sm:py-11 sm:pl-[6.25rem] sm:pr-[22.5rem]">
<LoginVisible staff>
<LoginVisible role={['ROLE_COUNCIL', 'ROLE_STAFF']}>
<div className="mb-8 text-right">
<EditButton href={`${councilPath}/edit`} />
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
/>{' '}
대 학생회{' '}
<Form.Text
Expand Down
91 changes: 91 additions & 0 deletions app/[locale]/community/council/rules/edit/client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
'use client';

import { FormProvider, useForm } from 'react-hook-form';

import { putCouncilRulesAction } from '@/actions/council';
import { CouncilRules } from '@/apis/v2/council/rule';
import Form from '@/components/form/Form';
import { useRouter } from '@/i18n/routing';
import { EditorFile } from '@/types/form';
import {
attachmentsToEditorFiles,
contentToFormData,
getAttachmentDeleteIds,
} from '@/utils/formData';

interface FormData {
constitutionAttachments: EditorFile[];
bylawAttachments: EditorFile[];
}

interface Props {
councilRules: CouncilRules;
}

export default function CouncilByLawsEditClientPage({ councilRules }: Props) {
const constitutionAttachments = councilRules.constitution.attachments;
const bylawAttachments = councilRules.bylaw.attachments;

const methods = useForm<FormData>({
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 (
<FormProvider {...methods}>
<Form>
<Form.Section title="학생회칙" mb="mb-10" titleMb="mb-2">
<Form.File name="constitutionAttachments" multiple rules={{ required: true }} />
</Form.Section>

<Form.Section title="세칙" titleMb="mb-2">
<Form.File name="bylawAttachments" multiple rules={{ required: true }} />
</Form.Section>

<Form.Action onCancel={onCancel} onSubmit={onSubmit} />
</Form>
</FormProvider>
);
}
13 changes: 13 additions & 0 deletions app/[locale]/community/council/rules/edit/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<PageLayout title="학생 회칙 및 세칙 수정" titleType="big" hideNavbar>
<CouncilByLawsEditClientPage councilRules={councilRules} />
</PageLayout>
);
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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 (
<PageLayout titleType="big">
<LoginVisible role={['ROLE_STAFF', 'ROLE_COUNCIL']}>
<div className="flex justify-end">
<EditButton href={editPath} />
</div>
</LoginVisible>

<h3 className="mb-[20px] text-[20px] font-semibold text-neutral-950">학생회칙</h3>

{resp.constitution.attachments.map((attachment) => (
Expand Down
24 changes: 13 additions & 11 deletions components/form/File.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
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';

import { EditorFile, LocalFile } from '../../types/form';

interface FilePickerProps {
name: string;
options?: RegisterOptions;
rules?: Omit<
RegisterOptions<FieldValues, string>,
'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<HTMLInputElement> = (e) => {
if (e.target.files === null) return;
Expand All @@ -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
Expand All @@ -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 (
Expand All @@ -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) => (
<FilePickerRow
// 순서를 안바꾸기로 했으니 키값으로 인덱스 써도 ㄱㅊ
key={idx}
Expand Down
14 changes: 10 additions & 4 deletions components/form/Text.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import clsx from 'clsx';
import { InputHTMLAttributes } from 'react';
import { RegisterOptions, useFormContext } from 'react-hook-form';

Expand All @@ -15,17 +16,22 @@ export default function Text({
textCenter,
name,
options,
className,
...props
}: BasicTextInputProps) {
const { register } = useFormContext();

return (
<input
type="text"
className={`${maxWidth} autofill-bg-white h-8 rounded-sm border border-neutral-300
${bgColor} pl-2 text-sm outline-none placeholder:text-neutral-300 disabled:text-neutral-400 ${
textCenter && 'pr-2 text-center'
}`}
className={clsx(
maxWidth,
'autofill-bg-white h-8 rounded-sm border border-neutral-300',
bgColor,
'pl-2 text-sm outline-none placeholder:text-neutral-300 disabled:text-neutral-400',
textCenter && 'pr-2 text-center',
className,
)}
{...props}
{...register(name, options)}
/>
Expand Down
1 change: 1 addition & 0 deletions constants/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
2 changes: 1 addition & 1 deletion constants/segmentNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ export const councilMinute: SegmentNode = {

export const councilBylaws: SegmentNode = {
name: '학생회칙 및 세칙',
segment: 'bylaws',
segment: 'rules',
isPage: true,
parent: council,
children: [],
Expand Down
8 changes: 8 additions & 0 deletions utils/formData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

0 comments on commit 08a3a55

Please sign in to comment.