Skip to content
This repository was archived by the owner on Jan 2, 2026. It is now read-only.

Commit 08a3a55

Browse files
authored
✨ 학생회칙 편집 페이지 (#325)
1 parent 5d8e8fb commit 08a3a55

File tree

12 files changed

+186
-23
lines changed

12 files changed

+186
-23
lines changed

actions/council.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,19 @@ import {
1010
} from '@/apis/v2/council/meeting-minute';
1111
import { postCouncilReport } from '@/apis/v2/council/report';
1212
import { deleteCouncilReport, putCouncilReport } from '@/apis/v2/council/report/[id]';
13+
import { putCouncilRules } from '@/apis/v2/council/rule';
1314
import {
1415
FETCH_TAG_COUNCIL_INTRO,
1516
FETCH_TAG_COUNCIL_MINUTE,
1617
FETCH_TAG_COUNCIL_REPORT,
18+
FETCH_TAG_COUNCIL_RULES,
1719
} from '@/constants/network';
18-
import { councilIntro, councilMinute, councilReportList } from '@/constants/segmentNode';
20+
import {
21+
councilBylaws as councilRules,
22+
councilIntro,
23+
councilMinute,
24+
councilReportList,
25+
} from '@/constants/segmentNode';
1926
import { redirectKo } from '@/i18n/routing';
2027
import { getPath } from '@/utils/page';
2128
import { decodeFormDataFileName } from '@/utils/string';
@@ -84,3 +91,22 @@ export const deleteCouncilReportAction = async (id: number) => {
8491
}
8592
redirectKo(councilReportPath);
8693
};
94+
95+
/** 학생회칙 */
96+
97+
const councilRulesPath = getPath(councilRules);
98+
99+
export const putCouncilRulesAction = withErrorHandler(
100+
async (bylawFormData?: FormData, constitutionFormData?: FormData) => {
101+
if (bylawFormData) decodeFormDataFileName(bylawFormData, 'newAttachments');
102+
if (constitutionFormData) decodeFormDataFileName(constitutionFormData, 'newAttachments');
103+
104+
await Promise.all([
105+
bylawFormData && putCouncilRules('bylaw', bylawFormData),
106+
constitutionFormData && putCouncilRules('constitution', constitutionFormData),
107+
]);
108+
109+
revalidateTag(FETCH_TAG_COUNCIL_RULES);
110+
redirectKo(councilRulesPath);
111+
},
112+
);

apis/v2/council/rule.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1-
import { getRequest } from '@/apis';
1+
import { getRequest, putRequest } from '@/apis';
22
import { Attachment } from '@/apis/types/attachment';
33

4-
type Response = {
4+
export type CouncilRules = {
55
constitution: { type: string; attachments: Attachment[] };
66
bylaw: { type: string; attachments: Attachment[] };
77
};
88

9-
export const getCouncilRule = () => getRequest<Response>('/v2/council/rule', undefined);
9+
export const getCouncilRules = () => getRequest<CouncilRules>('/v2/council/rule', undefined);
10+
11+
export const putCouncilRules = (type: keyof CouncilRules, body: FormData) =>
12+
putRequest<CouncilRules>(`/v2/council/rule/${type}`, { body, jsessionID: true });

app/[locale]/community/council/intro/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export default async function CouncilIntroPage() {
3333
return (
3434
<PageLayout titleType="big" removePadding>
3535
<div className="bg-neutral-100 px-5 pb-12 pt-7 sm:py-11 sm:pl-[6.25rem] sm:pr-[22.5rem]">
36-
<LoginVisible staff>
36+
<LoginVisible role={['ROLE_COUNCIL', 'ROLE_STAFF']}>
3737
<div className="mb-8 text-right">
3838
<EditButton href={`${councilPath}/edit`} />
3939
</div>

app/[locale]/community/council/report/components/CouncilReportEditor.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ export default function CouncilReportEditor({ onCancel, onSubmit, defaultValues
5050
maxWidth="w-[39px]"
5151
placeholder="39"
5252
options={{ required: true }}
53+
type="number"
54+
className="[appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
5355
/>{' '}
5456
대 학생회{' '}
5557
<Form.Text
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
'use client';
2+
3+
import { FormProvider, useForm } from 'react-hook-form';
4+
5+
import { putCouncilRulesAction } from '@/actions/council';
6+
import { CouncilRules } from '@/apis/v2/council/rule';
7+
import Form from '@/components/form/Form';
8+
import { useRouter } from '@/i18n/routing';
9+
import { EditorFile } from '@/types/form';
10+
import {
11+
attachmentsToEditorFiles,
12+
contentToFormData,
13+
getAttachmentDeleteIds,
14+
} from '@/utils/formData';
15+
16+
interface FormData {
17+
constitutionAttachments: EditorFile[];
18+
bylawAttachments: EditorFile[];
19+
}
20+
21+
interface Props {
22+
councilRules: CouncilRules;
23+
}
24+
25+
export default function CouncilByLawsEditClientPage({ councilRules }: Props) {
26+
const constitutionAttachments = councilRules.constitution.attachments;
27+
const bylawAttachments = councilRules.bylaw.attachments;
28+
29+
const methods = useForm<FormData>({
30+
defaultValues: {
31+
constitutionAttachments: attachmentsToEditorFiles(constitutionAttachments),
32+
bylawAttachments: attachmentsToEditorFiles(bylawAttachments),
33+
},
34+
});
35+
const router = useRouter();
36+
37+
const {
38+
handleSubmit,
39+
formState: { dirtyFields },
40+
} = methods;
41+
42+
const onCancel = () => {
43+
router.back();
44+
};
45+
46+
const onSubmit = handleSubmit(async (formData: FormData) => {
47+
const bylawsFormData = dirtyFields.bylawAttachments
48+
? (() => {
49+
const bylawsDeleteIds = getAttachmentDeleteIds(
50+
formData.bylawAttachments,
51+
bylawAttachments,
52+
);
53+
return contentToFormData('EDIT', {
54+
requestObject: { deleteIds: bylawsDeleteIds },
55+
attachments: formData.bylawAttachments,
56+
});
57+
})()
58+
: undefined;
59+
60+
const constitutionFormData = dirtyFields.constitutionAttachments
61+
? (() => {
62+
const constitutionDeleteIds = getAttachmentDeleteIds(
63+
formData.constitutionAttachments,
64+
constitutionAttachments,
65+
);
66+
return contentToFormData('EDIT', {
67+
requestObject: { deleteIds: constitutionDeleteIds },
68+
attachments: formData.constitutionAttachments,
69+
});
70+
})()
71+
: undefined;
72+
73+
await putCouncilRulesAction(bylawsFormData, constitutionFormData);
74+
});
75+
76+
return (
77+
<FormProvider {...methods}>
78+
<Form>
79+
<Form.Section title="학생회칙" mb="mb-10" titleMb="mb-2">
80+
<Form.File name="constitutionAttachments" multiple rules={{ required: true }} />
81+
</Form.Section>
82+
83+
<Form.Section title="세칙" titleMb="mb-2">
84+
<Form.File name="bylawAttachments" multiple rules={{ required: true }} />
85+
</Form.Section>
86+
87+
<Form.Action onCancel={onCancel} onSubmit={onSubmit} />
88+
</Form>
89+
</FormProvider>
90+
);
91+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { getCouncilRules } from '@/apis/v2/council/rule';
2+
import CouncilByLawsEditClientPage from '@/app/[locale]/community/council/rules/edit/client';
3+
import PageLayout from '@/components/layout/pageLayout/PageLayout';
4+
5+
export default async function CouncilBylawsEditPage() {
6+
const councilRules = await getCouncilRules();
7+
8+
return (
9+
<PageLayout title="학생 회칙 및 세칙 수정" titleType="big" hideNavbar>
10+
<CouncilByLawsEditClientPage councilRules={councilRules} />
11+
</PageLayout>
12+
);
13+
}

app/[locale]/community/council/bylaws/page.tsx renamed to app/[locale]/community/council/rules/page.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
export const dynamic = 'force-dynamic';
22

3-
import { getCouncilRule } from '@/apis/v2/council/rule';
3+
import { getCouncilRules } from '@/apis/v2/council/rule';
44
import CouncilAttachment from '@/app/[locale]/community/council/components/CouncilAttachments';
5+
import { EditButton } from '@/components/common/Buttons';
6+
import LoginVisible from '@/components/common/LoginVisible';
57
import PageLayout from '@/components/layout/pageLayout/PageLayout';
68
import { councilBylaws } from '@/constants/segmentNode';
79
import { Language } from '@/types/language';
810
import { getMetadata } from '@/utils/metadata';
11+
import { getPath } from '@/utils/page';
912

1013
export async function generateMetadata(props: { params: Promise<{ locale: Language }> }) {
1114
const params = await props.params;
@@ -14,11 +17,19 @@ export async function generateMetadata(props: { params: Promise<{ locale: Langua
1417
return await getMetadata({ locale, node: councilBylaws });
1518
}
1619

20+
const editPath = `${getPath(councilBylaws)}/edit`;
21+
1722
export default async function CouncilIntroPage() {
18-
const resp = await getCouncilRule();
23+
const resp = await getCouncilRules();
1924

2025
return (
2126
<PageLayout titleType="big">
27+
<LoginVisible role={['ROLE_STAFF', 'ROLE_COUNCIL']}>
28+
<div className="flex justify-end">
29+
<EditButton href={editPath} />
30+
</div>
31+
</LoginVisible>
32+
2233
<h3 className="mb-[20px] text-[20px] font-semibold text-neutral-950">학생회칙</h3>
2334

2435
{resp.constitution.attachments.map((attachment) => (

components/form/File.tsx

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,24 @@
11
import { ChangeEventHandler, MouseEventHandler } from 'react';
2-
import { RegisterOptions, useFormContext, useWatch } from 'react-hook-form';
2+
import { FieldValues, RegisterOptions, useController, useFormContext } from 'react-hook-form';
33

44
import ClearIcon from '@/public/image/clear_icon.svg';
55

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

88
interface FilePickerProps {
99
name: string;
10-
options?: RegisterOptions;
10+
rules?: Omit<
11+
RegisterOptions<FieldValues, string>,
12+
'setValueAs' | 'disabled' | 'valueAsNumber' | 'valueAsDate'
13+
>;
1114
multiple?: boolean;
1215
}
1316

14-
export default function FilePicker({ name, options, multiple = true }: FilePickerProps) {
15-
const { register, setValue } = useFormContext();
16-
const files = useWatch({ name }) as EditorFile[];
17-
18-
register(name, options);
19-
17+
export default function FilePicker({ name, rules, multiple = true }: FilePickerProps) {
18+
const { control } = useFormContext();
19+
const {
20+
field: { value: files, onChange },
21+
} = useController({ name, rules, control });
2022
// 성능 확인 필요
2123
const handleChange: ChangeEventHandler<HTMLInputElement> = (e) => {
2224
if (e.target.files === null) return;
@@ -26,7 +28,7 @@ export default function FilePicker({ name, options, multiple = true }: FilePicke
2628
file,
2729
}));
2830

29-
setValue(name, [...files, ...newFiles]);
31+
onChange([...files, ...newFiles]);
3032

3133
// 같은 파일에 대해서 선택이 가능하도록 처리
3234
// https://stackoverflow.com/a/12102992
@@ -36,7 +38,7 @@ export default function FilePicker({ name, options, multiple = true }: FilePicke
3638
const deleteFileAtIndex = (index: number) => {
3739
const nextFiles = [...files];
3840
nextFiles.splice(index, 1);
39-
setValue(name, nextFiles);
41+
onChange(nextFiles);
4042
};
4143

4244
return (
@@ -47,7 +49,7 @@ export default function FilePicker({ name, options, multiple = true }: FilePicke
4749
self-start rounded-sm border-[1px] border-neutral-200 bg-neutral-50
4850
`}
4951
>
50-
{files.map((item, idx) => (
52+
{(files as EditorFile[]).map((item, idx) => (
5153
<FilePickerRow
5254
// 순서를 안바꾸기로 했으니 키값으로 인덱스 써도 ㄱㅊ
5355
key={idx}

components/form/Text.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import clsx from 'clsx';
12
import { InputHTMLAttributes } from 'react';
23
import { RegisterOptions, useFormContext } from 'react-hook-form';
34

@@ -15,17 +16,22 @@ export default function Text({
1516
textCenter,
1617
name,
1718
options,
19+
className,
1820
...props
1921
}: BasicTextInputProps) {
2022
const { register } = useFormContext();
2123

2224
return (
2325
<input
2426
type="text"
25-
className={`${maxWidth} autofill-bg-white h-8 rounded-sm border border-neutral-300
26-
${bgColor} pl-2 text-sm outline-none placeholder:text-neutral-300 disabled:text-neutral-400 ${
27-
textCenter && 'pr-2 text-center'
28-
}`}
27+
className={clsx(
28+
maxWidth,
29+
'autofill-bg-white h-8 rounded-sm border border-neutral-300',
30+
bgColor,
31+
'pl-2 text-sm outline-none placeholder:text-neutral-300 disabled:text-neutral-400',
32+
textCenter && 'pr-2 text-center',
33+
className,
34+
)}
2935
{...props}
3036
{...register(name, options)}
3137
/>

constants/network.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,4 @@ export const FETCH_TAG_INTERNATIONAL_UNDERGRADUATE = 'international-undergraduat
5353
export const FETCH_TAG_COUNCIL_INTRO = 'council-intro';
5454
export const FETCH_TAG_COUNCIL_MINUTE = 'council-minute';
5555
export const FETCH_TAG_COUNCIL_REPORT = 'council-report';
56+
export const FETCH_TAG_COUNCIL_RULES = 'council-rules';

0 commit comments

Comments
 (0)