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

Commit 9156bdf

Browse files
authored
✨ 학생회 소개 페이지 구현 (#309)
1 parent cdd66a5 commit 9156bdf

File tree

9 files changed

+195
-1
lines changed

9 files changed

+195
-1
lines changed

actions/council.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
'use server';
2+
3+
import { revalidateTag } from 'next/cache';
4+
5+
import { putCouncilIntro } from '@/apis/v2/council/intro';
6+
import { FETCH_TAG_INTRO } from '@/constants/network';
7+
import { councilIntro } from '@/constants/segmentNode';
8+
import { redirectKo } from '@/i18n/routing';
9+
import { getPath } from '@/utils/page';
10+
11+
import { withErrorHandler } from './errorHandler';
12+
13+
const introPath = getPath(councilIntro);
14+
15+
export const putIntroAction = withErrorHandler(async (formData: FormData) => {
16+
await putCouncilIntro(formData);
17+
revalidateTag(FETCH_TAG_INTRO);
18+
redirectKo(introPath);
19+
});

apis/v2/council/intro.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { putRequest } from '@/apis';
2+
import { getRequest } from '@/apis';
3+
import { FETCH_TAG_INTRO } from '@/constants/network';
4+
5+
export const getCouncilIntro = () =>
6+
getRequest<{ description: string; imageURL: string }>('/v2/council/intro', undefined, {
7+
next: { tags: [FETCH_TAG_INTRO] },
8+
});
9+
10+
export const putCouncilIntro = (formData: FormData) =>
11+
putRequest('/v2/council/intro', { body: formData, jsessionID: true });
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
'use client';
2+
3+
import { FormProvider, useForm } from 'react-hook-form';
4+
5+
import Fieldset from '@/components/form/Fieldset';
6+
import Form from '@/components/form/Form';
7+
import { useRouter } from '@/i18n/routing';
8+
import { EditorImage } from '@/types/form';
9+
import { contentToFormData } from '@/utils/formData';
10+
import { handleServerResponse } from '@/utils/serverActionError';
11+
12+
export interface IntroFormData {
13+
description: string;
14+
image: EditorImage;
15+
}
16+
17+
interface Props {
18+
cancelPath: string;
19+
defaultValues: IntroFormData;
20+
onSubmit: (formData: FormData) => Promise<unknown>;
21+
}
22+
23+
export default function IntroEditor({ cancelPath, defaultValues, onSubmit: _onSubmit }: Props) {
24+
const router = useRouter();
25+
const formMethods = useForm<IntroFormData>({ defaultValues });
26+
const { handleSubmit } = formMethods;
27+
28+
const onCancel = () => router.push(cancelPath);
29+
30+
const onSubmit = handleSubmit(async ({ description, image }) => {
31+
const requestObject = {
32+
description,
33+
sequence: 0,
34+
name: 'name',
35+
removeImage: defaultValues.image !== null && image === null,
36+
};
37+
38+
const formData = contentToFormData('EDIT', { requestObject, image });
39+
const resp = await _onSubmit(formData);
40+
handleServerResponse(resp, { successMessage: '수정 완료되었습니다.' });
41+
});
42+
43+
return (
44+
<FormProvider {...formMethods}>
45+
<Form>
46+
<Fieldset.HTML>
47+
<Form.HTML name="description" options={{ required: true }} />
48+
</Fieldset.HTML>
49+
50+
<Fieldset.Image>
51+
<label className="mb-3 whitespace-pre-wrap text-sm font-normal tracking-wide text-neutral-500">
52+
학생회 조직도
53+
</label>
54+
<Form.Image name="image" />
55+
</Fieldset.Image>
56+
57+
<Form.Action onCancel={onCancel} onSubmit={onSubmit} />
58+
</Form>
59+
</FormProvider>
60+
);
61+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { putIntroAction } from '@/actions/council';
2+
import { getCouncilIntro } from '@/apis/v2/council/intro';
3+
import PageLayout from '@/components/layout/pageLayout/PageLayout';
4+
import { councilIntro } from '@/constants/segmentNode';
5+
import { getEditorImage } from '@/utils/formData';
6+
import { getPath } from '@/utils/page';
7+
8+
import IntroEditor, { IntroFormData } from './IntroEditor';
9+
10+
const path = getPath(councilIntro);
11+
12+
export default async function IntroEditPage() {
13+
const data = await getCouncilIntro();
14+
15+
const defaultValues: IntroFormData = {
16+
description: data.description,
17+
image: getEditorImage(data.imageURL),
18+
};
19+
20+
return (
21+
<PageLayout title="학생회 소개 편집" titleType="big" hideNavbar>
22+
<IntroEditor cancelPath={path} defaultValues={defaultValues} onSubmit={putIntroAction} />
23+
</PageLayout>
24+
);
25+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
export const dynamic = 'force-dynamic';
2+
3+
import { getCouncilIntro } from '@/apis/v2/council/intro';
4+
import { EditButton } from '@/components/common/Buttons';
5+
import Image from '@/components/common/Image';
6+
import LoginVisible from '@/components/common/LoginVisible';
7+
import HTMLViewer from '@/components/form/html/HTMLViewer';
8+
import PageLayout from '@/components/layout/pageLayout/PageLayout';
9+
import { councilIntro } from '@/constants/segmentNode';
10+
import { Language } from '@/types/language';
11+
import { getMetadata } from '@/utils/metadata';
12+
import { getPath } from '@/utils/page';
13+
14+
export async function generateMetadata(props: { params: Promise<{ locale: Language }> }) {
15+
const params = await props.params;
16+
17+
const { locale } = params;
18+
19+
const { imageURL } = await getCouncilIntro();
20+
21+
return await getMetadata({
22+
locale,
23+
node: councilIntro,
24+
metadata: { openGraph: { images: imageURL ? [imageURL] : undefined } },
25+
});
26+
}
27+
28+
const councilPath = getPath(councilIntro);
29+
30+
export default async function CouncilIntroPage() {
31+
const { description, imageURL } = await getCouncilIntro();
32+
33+
return (
34+
<PageLayout titleType="big" removePadding>
35+
<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>
37+
<div className="mb-8 text-right">
38+
<EditButton href={`${councilPath}/edit`} />
39+
</div>
40+
</LoginVisible>
41+
<HTMLViewer htmlContent={description} />
42+
</div>
43+
<div className="px-5 pb-16 pt-10 sm:pl-[6.25rem] sm:pr-[22.5rem]">
44+
<h2 className="mb-6 text-base font-semibold">조직도</h2>
45+
<Image
46+
src={imageURL}
47+
alt="학생회_구성도"
48+
width={580}
49+
height={435}
50+
className="w-full object-contain sm:w-[580px]"
51+
/>
52+
</div>
53+
</PageLayout>
54+
);
55+
}

constants/network.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,5 @@ export const FETCH_TAG_EXCHANGE = 'exchange-visiting';
4949
export const FETCH_TAG_INTERNATIONAL_SCHOLARSHIPS = 'international-scholarships';
5050
export const FETCH_TAG_INTERNATIONAL_GRADUATE = 'international-graduate';
5151
export const FETCH_TAG_INTERNATIONAL_UNDERGRADUATE = 'international-undergraduate';
52+
53+
export const FETCH_TAG_INTRO = 'intro';

constants/segmentNode.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,22 @@ export const facultyRecruitment: SegmentNode = {
126126
children: [],
127127
};
128128

129+
export const council: SegmentNode = {
130+
name: '학생회',
131+
segment: 'council',
132+
isPage: true,
133+
parent: community,
134+
children: [],
135+
};
136+
137+
export const councilIntro: SegmentNode = {
138+
name: '학생회 소개',
139+
segment: 'intro',
140+
isPage: true,
141+
parent: council,
142+
children: [],
143+
};
144+
129145
export const people: SegmentNode = {
130146
name: '구성원',
131147
segment: 'people',
@@ -577,7 +593,8 @@ about.children = [
577593
contact,
578594
directions,
579595
];
580-
community.children = [notice, news, seminar, facultyRecruitment];
596+
community.children = [notice, news, seminar, facultyRecruitment, council];
597+
council.children = [councilIntro];
581598
people.children = [faculty, emeritusFaculty, staff];
582599
research.children = [researchGroups, researchCenters, researchLabs, topConferenceList];
583600
admissions.children = [undergraduateAdmission, graduateAdmission, internationalAdmission];

messages/en.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
"새 소식": "News",
2828
"세미나": "Seminars",
2929
"신임교수초빙": "Faculty Recruitment",
30+
"학생회": "Student Council",
31+
"학생회 소개": "Student Council Introduction",
3032

3133
"교수진": "Faculty",
3234
"역대 교수진": "Emeritus Faculty",

messages/ko.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
"새 소식": "새 소식",
2828
"세미나": "세미나",
2929
"신임교수초빙": "신임교수초빙",
30+
"학생회": "학생회",
31+
"학생회 소개": "학생회 소개",
3032

3133
"교수진": "교수진",
3234
"역대 교수진": "역대 교수진",

0 commit comments

Comments
 (0)