From e01f8f1cec9b1ec6f61ef74e8cf4385de31ad5d7 Mon Sep 17 00:00:00 2001 From: chansol Date: Thu, 20 Feb 2025 22:41:22 +0900 Subject: [PATCH 01/18] =?UTF-8?q?feat:=20api=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- actions/council.ts | 36 +++++++++++++++++++++++++++++-- apis/types/council.ts | 7 ++++++ apis/v2/council/meeting-minute.ts | 24 +++++++++++++++++++++ constants/network.ts | 1 + constants/segmentNode.ts | 10 ++++++++- messages/en.json | 1 + messages/ko.json | 1 + 7 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 apis/types/council.ts create mode 100644 apis/v2/council/meeting-minute.ts diff --git a/actions/council.ts b/actions/council.ts index 0a29d6e1..535861f2 100644 --- a/actions/council.ts +++ b/actions/council.ts @@ -3,13 +3,20 @@ import { revalidateTag } from 'next/cache'; import { putCouncilIntro } from '@/apis/v2/council/intro'; -import { FETCH_TAG_INTRO } from '@/constants/network'; -import { councilIntro } from '@/constants/segmentNode'; +import { + deleteCouncilMinute, + postCouncilMinutesByYear, + putCouncilMinute, +} from '@/apis/v2/council/meeting-minute'; +import { FETCH_TAG_INTRO, FETCH_TAG_MINUTE } from '@/constants/network'; +import { councilIntro, councilMinute } from '@/constants/segmentNode'; import { redirectKo } from '@/i18n/routing'; import { getPath } from '@/utils/page'; import { withErrorHandler } from './errorHandler'; +/** 소개 */ + const introPath = getPath(councilIntro); export const putIntroAction = withErrorHandler(async (formData: FormData) => { @@ -17,3 +24,28 @@ export const putIntroAction = withErrorHandler(async (formData: FormData) => { revalidateTag(FETCH_TAG_INTRO); redirectKo(introPath); }); + +/** 회의록 */ + +const minutePath = getPath(councilMinute); + +export const postMinutesByYearAction = withErrorHandler( + async (year: number, formData: FormData) => { + await postCouncilMinutesByYear(year, formData); + revalidateTag(FETCH_TAG_MINUTE); + redirectKo(minutePath); + }, +); + +export const putMinuteAction = withErrorHandler( + async (year: number, index: number, formData: FormData) => { + await putCouncilMinute(year, index, formData); + revalidateTag(FETCH_TAG_MINUTE); + redirectKo(minutePath); + }, +); + +export const deleteMinuteAction = withErrorHandler(async (year: number, index: number) => { + await deleteCouncilMinute(year, index); + revalidateTag(FETCH_TAG_MINUTE); +}); diff --git a/apis/types/council.ts b/apis/types/council.ts new file mode 100644 index 00000000..21cf8692 --- /dev/null +++ b/apis/types/council.ts @@ -0,0 +1,7 @@ +import { Attachment } from '@/components/common/Attachments'; + +export interface Minute { + year: number; + index: number; + attachments: Attachment[]; +} diff --git a/apis/v2/council/meeting-minute.ts b/apis/v2/council/meeting-minute.ts new file mode 100644 index 00000000..ed33aa52 --- /dev/null +++ b/apis/v2/council/meeting-minute.ts @@ -0,0 +1,24 @@ +import { deleteRequest, postRequest, putRequest } from '@/apis'; +import { getRequest } from '@/apis'; +import { Minute } from '@/apis/types/council'; +import { FETCH_TAG_MINUTE } from '@/constants/network'; + +export const getCouncilMinutesByYear = (year: number) => + getRequest(`/v2/council/meeting-minute/${year}`, undefined, { + next: { tags: [FETCH_TAG_MINUTE] }, + }); + +export const postCouncilMinutesByYear = (year: number, formData: FormData) => { + postRequest(`/v2/council/meeting-minute/${year}`, { body: formData, jsessionID: true }); +}; + +export const getCouncilMinute = (year: number, index: number) => + getRequest(`/v2/council/meeting-minute/${year}/${index}`, undefined, { + next: { tags: [FETCH_TAG_MINUTE] }, + }); + +export const putCouncilMinute = (year: number, index: number, formData: FormData) => + putRequest(`/v2/council/meeting-minute/${year}/${index}`, { body: formData, jsessionID: true }); + +export const deleteCouncilMinute = async (year: number, index: number) => + deleteRequest(`/v2/council/meeting-minute/${year}/${index}`, { jsessionID: true }); diff --git a/constants/network.ts b/constants/network.ts index d9763704..f052b94b 100644 --- a/constants/network.ts +++ b/constants/network.ts @@ -51,3 +51,4 @@ export const FETCH_TAG_INTERNATIONAL_GRADUATE = 'international-graduate'; export const FETCH_TAG_INTERNATIONAL_UNDERGRADUATE = 'international-undergraduate'; export const FETCH_TAG_INTRO = 'intro'; +export const FETCH_TAG_MINUTE = 'minute'; diff --git a/constants/segmentNode.ts b/constants/segmentNode.ts index be5a33a0..aa80bee3 100644 --- a/constants/segmentNode.ts +++ b/constants/segmentNode.ts @@ -142,6 +142,14 @@ export const councilIntro: SegmentNode = { children: [], }; +export const councilMinute: SegmentNode = { + name: '학생회 회의록', + segment: 'meeting-minute', + isPage: true, + parent: council, + children: [], +}; + export const people: SegmentNode = { name: '구성원', segment: 'people', @@ -594,7 +602,7 @@ about.children = [ directions, ]; community.children = [notice, news, seminar, facultyRecruitment, council]; -council.children = [councilIntro]; +council.children = [councilIntro, councilMinute]; people.children = [faculty, emeritusFaculty, staff]; research.children = [researchGroups, researchCenters, researchLabs, topConferenceList]; admissions.children = [undergraduateAdmission, graduateAdmission, internationalAdmission]; diff --git a/messages/en.json b/messages/en.json index bff14e4b..74e42498 100644 --- a/messages/en.json +++ b/messages/en.json @@ -29,6 +29,7 @@ "신임교수초빙": "Faculty Recruitment", "학생회": "Student Council", "학생회 소개": "Student Council Introduction", + "학생회 회의록": "Student Council Minutes", "교수진": "Faculty", "역대 교수진": "Emeritus Faculty", diff --git a/messages/ko.json b/messages/ko.json index c3a39019..0dca7489 100644 --- a/messages/ko.json +++ b/messages/ko.json @@ -29,6 +29,7 @@ "신임교수초빙": "신임교수초빙", "학생회": "학생회", "학생회 소개": "학생회 소개", + "학생회 회의록": "학생회 회의록", "교수진": "교수진", "역대 교수진": "역대 교수진", From 520aaf0ae3c417a8465caac9dffa6ce8f19a9937 Mon Sep 17 00:00:00 2001 From: chansol Date: Thu, 20 Feb 2025 23:01:12 +0900 Subject: [PATCH 02/18] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9D=98=EB=A1=9D=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=ED=83=80=EC=9E=84=EB=9D=BC?= =?UTF-8?q?=EC=9D=B8=20=EC=9E=84=EC=8B=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apis/v2/council/meeting-minute.ts | 3 +- .../council/minute/MinutePageContent.tsx | 155 ++++++++++++++++++ .../community/council/minute/page.tsx | 19 +++ 3 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 app/[locale]/community/council/minute/MinutePageContent.tsx create mode 100644 app/[locale]/community/council/minute/page.tsx diff --git a/apis/v2/council/meeting-minute.ts b/apis/v2/council/meeting-minute.ts index ed33aa52..61ed6564 100644 --- a/apis/v2/council/meeting-minute.ts +++ b/apis/v2/council/meeting-minute.ts @@ -8,9 +8,8 @@ export const getCouncilMinutesByYear = (year: number) => next: { tags: [FETCH_TAG_MINUTE] }, }); -export const postCouncilMinutesByYear = (year: number, formData: FormData) => { +export const postCouncilMinutesByYear = (year: number, formData: FormData) => postRequest(`/v2/council/meeting-minute/${year}`, { body: formData, jsessionID: true }); -}; export const getCouncilMinute = (year: number, index: number) => getRequest(`/v2/council/meeting-minute/${year}/${index}`, undefined, { diff --git a/app/[locale]/community/council/minute/MinutePageContent.tsx b/app/[locale]/community/council/minute/MinutePageContent.tsx new file mode 100644 index 00000000..026ac158 --- /dev/null +++ b/app/[locale]/community/council/minute/MinutePageContent.tsx @@ -0,0 +1,155 @@ +'use client'; + +import { useReducer, useState } from 'react'; + +import { deleteMinuteAction } from '@/actions/council'; +import Timeline from '@/app/[locale]/academics/components/timeline/Timeline'; +import Attachments, { Attachment } from '@/components/common/Attachments'; +import { DeleteButton, EditButton } from '@/components/common/Buttons'; +import LoginVisible from '@/components/common/LoginVisible'; +import PageLayout from '@/components/layout/pageLayout/PageLayout'; +import { Link, usePathname } from '@/i18n/routing'; +import { refreshPage } from '@/utils/refreshPage'; +import { CustomError, handleServerResponse } from '@/utils/serverActionError'; + +const YEAR_LIMIT_COUNT = 10; + +interface MinuteContent { + year: number; + attachments: Attachment[]; +} + +export default function MinutePageContent({ contents }: { contents: MinuteContent[] }) { + const [selectedYear, setSelectedYear] = useState(contents[0].year); + const timeLineYears = contents.map((change) => change.year).slice(0, YEAR_LIMIT_COUNT); + const yearLimit = timeLineYears.at(-1) ?? 0; + const selectedContents = getSelectedContents(selectedYear, yearLimit, contents); + const pathname = usePathname(); + + const handleDelete = async (year: number) => { + const index = 1; + + const resp = await deleteMinuteAction(year, index); + handleServerResponse(resp, { + successMessage: `${year}대 학생회 ${index}차 회의록을 삭제했습니다.`, + onSuccess: refreshPage, + }); + }; + + const getEditHref = (year: number) => `${pathname}/edit?year=${year}`; + + return ( + + + +
+ {selectedContents.length === 1 ? ( + handleDelete(selectedYear)} + editHref={getEditHref(selectedYear)} + /> + ) : ( + selectedContents.map((change, i) => ( + handleDelete(change.year)} + editHref={getEditHref(change.year)} + key={change.year} + /> + )) + )} +
+
+ ); +} + +function AddButton({ pathname }: { pathname: string }) { + return ( + + + add + 학생회 추가 + + + ); +} + +function Buttons({ + onDelete, + editHref, +}: { + onDelete: () => Promise; + editHref: string; +}) { + return ( + +
+ + +
+
+ ); +} + +function ContentViewer({ + attachments, + onDelete, + editHref, +}: { + attachments: Attachment[]; + onDelete: () => Promise; + editHref: string; +}) { + return ( +
+ + +
+ ); +} + +function TogglableContentViewer({ + expandDefault = false, + attachments, + onDelete, + editHref, +}: { + expandDefault?: boolean; + attachments: Attachment[]; + onDelete: () => Promise; + editHref: string; +}) { + const [isExpanded, toggleContent] = useReducer((x) => !x, expandDefault); + + return ( +
+ + {isExpanded && ( + <> + + + + )} +
+ ); +} + +const getSelectedContents = (year: number, yearLimit: number, data: MinuteContent[]) => { + if (year <= yearLimit) return data.filter((d) => d.year <= yearLimit); + + const change = data.find((d) => d.year === year); + return change ? [change] : [{ year, attachments: [] }]; +}; diff --git a/app/[locale]/community/council/minute/page.tsx b/app/[locale]/community/council/minute/page.tsx new file mode 100644 index 00000000..c6f62a1a --- /dev/null +++ b/app/[locale]/community/council/minute/page.tsx @@ -0,0 +1,19 @@ +import { getCouncilMinutesByYear } from '@/apis/v2/council/meeting-minute'; +import { councilMinute } from '@/constants/segmentNode'; +import { getMetadata } from '@/utils/metadata'; + +import MinutePageContent from './MinutePageContent'; + +export async function generateMetadata(props: { params: Promise<{ locale: string }> }) { + const params = await props.params; + + const { locale } = params; + + return await getMetadata({ locale, node: councilMinute }); +} + +export default async function CouncilMinutePage() { + const data = await getCouncilMinutesByYear(2020); + + return ; +} From dd7f0014e8d87e23c6296585d428e8cdd718199f Mon Sep 17 00:00:00 2001 From: Limchansol Date: Fri, 21 Feb 2025 16:00:49 +0900 Subject: [PATCH 03/18] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9D=98=EB=A1=9D=20?= =?UTF-8?q?=EC=97=B0=EB=8F=84=20=EC=B6=94=EA=B0=80=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../community/council/minute/MinuteEditor.tsx | 72 +++++++++++++++++++ .../council/minute/MinutePageContent.tsx | 2 +- .../community/council/minute/create/page.tsx | 14 ++++ .../community/council/minute/page.tsx | 31 +++++++- constants/segmentNode.ts | 2 +- 5 files changed, 116 insertions(+), 5 deletions(-) create mode 100644 app/[locale]/community/council/minute/MinuteEditor.tsx create mode 100644 app/[locale]/community/council/minute/create/page.tsx diff --git a/app/[locale]/community/council/minute/MinuteEditor.tsx b/app/[locale]/community/council/minute/MinuteEditor.tsx new file mode 100644 index 00000000..ee9035cf --- /dev/null +++ b/app/[locale]/community/council/minute/MinuteEditor.tsx @@ -0,0 +1,72 @@ +'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 { EditorFile, isLocalFile } from '@/types/form'; +import { handleServerResponse } from '@/utils/serverActionError'; +import { encodeFormDataFileName } from '@/utils/string'; +import { postMinutesByYearAction } from '@/actions/council'; + +export type MinutelineFormData = { year: number; file: EditorFile[] }; + +interface Props { + defaultValues?: MinutelineFormData; + // onSubmit: (data: FormData) => Promise; + cancelPath: string; +} + +export default function MinuteEditor({ defaultValues, cancelPath }: Props) { + const formMethods = useForm({ + defaultValues: defaultValues ?? { + year: new Date().getFullYear() + 1, + file: [], + }, + }); + const { handleSubmit } = formMethods; + + const router = useRouter(); + const onCancel = () => router.push(cancelPath); + + const onSubmit = async (requestObject: MinutelineFormData) => { + const formData = contentToFormData(requestObject.file); + + const resp = await postMinutesByYearAction(requestObject.year, formData); + handleServerResponse(resp, { successMessage: '저장되었습니다.' }); + }; + + return ( + +
+
+ +
+ + + + + +
+ ); +} + +const contentToFormData = (attachments: EditorFile[]) => { + const formData = new FormData(); + + if (attachments) { + encodeFormDataFileName( + formData, + 'attachments', + attachments.filter(isLocalFile).map((x) => x.file), + ); + } + + return formData; +}; diff --git a/app/[locale]/community/council/minute/MinutePageContent.tsx b/app/[locale]/community/council/minute/MinutePageContent.tsx index 026ac158..142313f3 100644 --- a/app/[locale]/community/council/minute/MinutePageContent.tsx +++ b/app/[locale]/community/council/minute/MinutePageContent.tsx @@ -31,7 +31,7 @@ export default function MinutePageContent({ contents }: { contents: MinuteConten const resp = await deleteMinuteAction(year, index); handleServerResponse(resp, { - successMessage: `${year}대 학생회 ${index}차 회의록을 삭제했습니다.`, + successMessage: `${year}년 ${index}차 회의록을 삭제했습니다.`, onSuccess: refreshPage, }); }; diff --git a/app/[locale]/community/council/minute/create/page.tsx b/app/[locale]/community/council/minute/create/page.tsx new file mode 100644 index 00000000..f4b6ed57 --- /dev/null +++ b/app/[locale]/community/council/minute/create/page.tsx @@ -0,0 +1,14 @@ +import PageLayout from '@/components/layout/pageLayout/PageLayout'; +import { councilMinute } from '@/constants/segmentNode'; +import { getPath } from '@/utils/page'; +import MinuteEditor from '../MinuteEditor'; + +const minutePath = getPath(councilMinute); + +export default function CouncilMinuteCreatePage() { + return ( + + + + ); +} diff --git a/app/[locale]/community/council/minute/page.tsx b/app/[locale]/community/council/minute/page.tsx index c6f62a1a..7e868a03 100644 --- a/app/[locale]/community/council/minute/page.tsx +++ b/app/[locale]/community/council/minute/page.tsx @@ -2,7 +2,10 @@ import { getCouncilMinutesByYear } from '@/apis/v2/council/meeting-minute'; import { councilMinute } from '@/constants/segmentNode'; import { getMetadata } from '@/utils/metadata'; -import MinutePageContent from './MinutePageContent'; +import PageLayout from '@/components/layout/pageLayout/PageLayout'; +import LoginVisible from '@/components/common/LoginVisible'; +import { Link } from '@/i18n/routing'; +import { getPath } from '@/utils/page'; export async function generateMetadata(props: { params: Promise<{ locale: string }> }) { const params = await props.params; @@ -12,8 +15,30 @@ export async function generateMetadata(props: { params: Promise<{ locale: string return await getMetadata({ locale, node: councilMinute }); } +const path = getPath(councilMinute); + export default async function CouncilMinutePage() { - const data = await getCouncilMinutesByYear(2020); + const data = await getCouncilMinutesByYear(2025); + console.log(data); + // return ; + return ( + + +
hihi
+
+ ); +} - return ; +function AddButton({ pathname }: { pathname: string }) { + return ( + + + add + 연도 추가 + + + ); } diff --git a/constants/segmentNode.ts b/constants/segmentNode.ts index aa80bee3..503a65d5 100644 --- a/constants/segmentNode.ts +++ b/constants/segmentNode.ts @@ -144,7 +144,7 @@ export const councilIntro: SegmentNode = { export const councilMinute: SegmentNode = { name: '학생회 회의록', - segment: 'meeting-minute', + segment: 'minute', isPage: true, parent: council, children: [], From af54cf949cbfce2b7bd331abb870bc4df3065630 Mon Sep 17 00:00:00 2001 From: Limchansol Date: Fri, 21 Feb 2025 17:03:38 +0900 Subject: [PATCH 04/18] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9D=98=EB=A1=9D=20?= =?UTF-8?q?=EB=B7=B0=EC=96=B4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../council/minute/MinutePageContent.tsx | 63 +++++++++---------- .../community/council/minute/page.tsx | 29 +++------ components/common/Attachments.tsx | 14 ++++- 3 files changed, 52 insertions(+), 54 deletions(-) diff --git a/app/[locale]/community/council/minute/MinutePageContent.tsx b/app/[locale]/community/council/minute/MinutePageContent.tsx index 142313f3..b2470b14 100644 --- a/app/[locale]/community/council/minute/MinutePageContent.tsx +++ b/app/[locale]/community/council/minute/MinutePageContent.tsx @@ -11,19 +11,28 @@ import PageLayout from '@/components/layout/pageLayout/PageLayout'; import { Link, usePathname } from '@/i18n/routing'; import { refreshPage } from '@/utils/refreshPage'; import { CustomError, handleServerResponse } from '@/utils/serverActionError'; +import { councilMinute } from '@/constants/segmentNode'; +import { getPath } from '@/utils/page'; const YEAR_LIMIT_COUNT = 10; interface MinuteContent { year: number; + index: number; attachments: Attachment[]; } +const TIME_LINE_YEARS = [2025, 2024, 2023]; // temp + +const minutePath = getPath(councilMinute); + export default function MinutePageContent({ contents }: { contents: MinuteContent[] }) { const [selectedYear, setSelectedYear] = useState(contents[0].year); - const timeLineYears = contents.map((change) => change.year).slice(0, YEAR_LIMIT_COUNT); + // const timeLineYears = contents.map((change) => change.year).slice(0, YEAR_LIMIT_COUNT); + const timeLineYears = TIME_LINE_YEARS; const yearLimit = timeLineYears.at(-1) ?? 0; - const selectedContents = getSelectedContents(selectedYear, yearLimit, contents); + // const selectedContents = getSelectedContents(selectedYear, yearLimit, contents); + const selectedContents = contents; const pathname = usePathname(); const handleDelete = async (year: number) => { @@ -47,23 +56,7 @@ export default function MinutePageContent({ contents }: { contents: MinuteConten setSelectedTime={setSelectedYear} />
- {selectedContents.length === 1 ? ( - handleDelete(selectedYear)} - editHref={getEditHref(selectedYear)} - /> - ) : ( - selectedContents.map((change, i) => ( - handleDelete(change.year)} - editHref={getEditHref(change.year)} - key={change.year} - /> - )) - )} +
); @@ -77,7 +70,7 @@ function AddButton({ pathname }: { pathname: string }) { className="mb-7 ml-0.5 flex h-[30px] w-fit items-center rounded-2xl border border-main-orange pl-0.5 pr-2 pt-px text-md text-main-orange duration-200 hover:bg-main-orange hover:text-white" > add - 학생회 추가 + 연도 추가 ); @@ -92,7 +85,7 @@ function Buttons({ }) { return ( -
+
@@ -100,19 +93,25 @@ function Buttons({ ); } -function ContentViewer({ - attachments, - onDelete, - editHref, -}: { - attachments: Attachment[]; - onDelete: () => Promise; - editHref: string; -}) { +function ContentViewer({ contents }: { contents: MinuteContent[] }) { return (
- - + {contents.map((minute) => { + return ( +
+
+
{minute.index}차 회의 회의록
+ deleteMinuteAction(minute.year, minute.index)} + editHref={`${minutePath}/edit/${minute.year}/${minute.index}`} + /> +
+ {minute.attachments.map((file) => ( + + ))} +
+ ); + })}
); } diff --git a/app/[locale]/community/council/minute/page.tsx b/app/[locale]/community/council/minute/page.tsx index 7e868a03..f9a0d754 100644 --- a/app/[locale]/community/council/minute/page.tsx +++ b/app/[locale]/community/council/minute/page.tsx @@ -7,6 +7,8 @@ import LoginVisible from '@/components/common/LoginVisible'; import { Link } from '@/i18n/routing'; import { getPath } from '@/utils/page'; +import MinutePageContent from './MinutePageContent'; + export async function generateMetadata(props: { params: Promise<{ locale: string }> }) { const params = await props.params; @@ -20,25 +22,12 @@ const path = getPath(councilMinute); export default async function CouncilMinutePage() { const data = await getCouncilMinutesByYear(2025); console.log(data); - // return ; - return ( - - -
hihi
-
- ); -} -function AddButton({ pathname }: { pathname: string }) { - return ( - - - add - 연도 추가 - - - ); + return ; + // return ( + // + // + //
hihi
+ //
+ // ); } diff --git a/components/common/Attachments.tsx b/components/common/Attachments.tsx index 0f5e2d0d..503d3608 100644 --- a/components/common/Attachments.tsx +++ b/components/common/Attachments.tsx @@ -8,11 +8,21 @@ export interface Attachment { } // TODO: 여러 맥락에서 사용되므로 마진 prop으로 건네주기 -export default function Attachments({ files }: { files: Attachment[] }) { +export default function Attachments({ + files, + margin = 'mb-9 mt-3 sm:mb-11 sm:mt-5', + padding = 'py-3 pl-4 pr-20 sm:pr-[10rem]', +}: { + files: Attachment[]; + margin?: string; + padding?: string; +}) { if (files.length === 0) return <>; return ( -
+
{files.map((file, index) => ( ))} From 0e86011b2f9ea81070146ec63929291abfe8eecf Mon Sep 17 00:00:00 2001 From: Limchansol Date: Fri, 21 Feb 2025 17:13:01 +0900 Subject: [PATCH 05/18] =?UTF-8?q?feat:=20togglable=20=EB=B7=B0=EC=96=B4=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../council/minute/MinutePageContent.tsx | 51 +++++++++---------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/app/[locale]/community/council/minute/MinutePageContent.tsx b/app/[locale]/community/council/minute/MinutePageContent.tsx index b2470b14..751bf23c 100644 --- a/app/[locale]/community/council/minute/MinutePageContent.tsx +++ b/app/[locale]/community/council/minute/MinutePageContent.tsx @@ -93,24 +93,28 @@ function Buttons({ ); } +function Minutes({ minute }: { minute: MinuteContent }) { + return ( +
+
+
{minute.index}차 회의 회의록
+ deleteMinuteAction(minute.year, minute.index)} + editHref={`${minutePath}/edit/${minute.year}/${minute.index}`} + /> +
+ {minute.attachments.map((file) => ( + + ))} +
+ ); +} + function ContentViewer({ contents }: { contents: MinuteContent[] }) { return (
{contents.map((minute) => { - return ( -
-
-
{minute.index}차 회의 회의록
- deleteMinuteAction(minute.year, minute.index)} - editHref={`${minutePath}/edit/${minute.year}/${minute.index}`} - /> -
- {minute.attachments.map((file) => ( - - ))} -
- ); + return ; })}
); @@ -118,14 +122,10 @@ function ContentViewer({ contents }: { contents: MinuteContent[] }) { function TogglableContentViewer({ expandDefault = false, - attachments, - onDelete, - editHref, + contents, }: { expandDefault?: boolean; - attachments: Attachment[]; - onDelete: () => Promise; - editHref: string; + contents: MinuteContent[]; }) { const [isExpanded, toggleContent] = useReducer((x) => !x, expandDefault); @@ -135,13 +135,12 @@ function TogglableContentViewer({ {isExpanded ? 'expand_less' : 'expand_more'} + {contents[0].year}년 회의록 - {isExpanded && ( - <> - - - - )} + {isExpanded && + contents.map((minute) => { + return ; + })}
); } From 058c91162bf889b4875929dbcffd976c6cb73ae2 Mon Sep 17 00:00:00 2001 From: chansol Date: Mon, 24 Feb 2025 23:27:54 +0900 Subject: [PATCH 06/18] =?UTF-8?q?fix:=20=ED=9A=8C=EC=9D=98=EB=A1=9D=20?= =?UTF-8?q?=ED=83=80=EC=9E=84=EB=9D=BC=EC=9D=B8=20=EB=B7=B0=EC=96=B4=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=ED=95=98=EC=97=AC=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apis/v2/council/meeting-minute.ts | 9 ++ .../council/minute/MinutePageContent.tsx | 112 +++++------------- .../community/council/minute/page.tsx | 17 +-- 3 files changed, 43 insertions(+), 95 deletions(-) diff --git a/apis/v2/council/meeting-minute.ts b/apis/v2/council/meeting-minute.ts index 88c9db25..319463f9 100644 --- a/apis/v2/council/meeting-minute.ts +++ b/apis/v2/council/meeting-minute.ts @@ -3,6 +3,15 @@ import { getRequest } from '@/apis'; import { Minute } from '@/apis/types/council'; import { FETCH_TAG_COUNCIL_MINUTE } from '@/constants/network'; +interface GETMinutesResponse { + [year: string]: Minute[]; +} + +export const getCouncilMinutes = () => + getRequest(`/v2/council/meeting-minute`, undefined, { + next: { tags: [FETCH_TAG_COUNCIL_MINUTE] }, + }); + export const getCouncilMinutesByYear = (year: number) => getRequest(`/v2/council/meeting-minute/${year}`, undefined, { next: { tags: [FETCH_TAG_COUNCIL_MINUTE] }, diff --git a/app/[locale]/community/council/minute/MinutePageContent.tsx b/app/[locale]/community/council/minute/MinutePageContent.tsx index 33cb789f..c69a876c 100644 --- a/app/[locale]/community/council/minute/MinutePageContent.tsx +++ b/app/[locale]/community/council/minute/MinutePageContent.tsx @@ -1,52 +1,36 @@ 'use client'; -import { useReducer, useState } from 'react'; +import { useState } from 'react'; import { deleteMinuteAction } from '@/actions/council'; -import { Attachment } from '@/apis/types/attachment'; +import { Minute } from '@/apis/types/council'; import Timeline from '@/app/[locale]/academics/components/timeline/Timeline'; -import Attachments from '@/components/common/Attachments'; import { DeleteButton, EditButton } from '@/components/common/Buttons'; import LoginVisible from '@/components/common/LoginVisible'; import PageLayout from '@/components/layout/pageLayout/PageLayout'; +import { councilMinute } from '@/constants/segmentNode'; import { Link, usePathname } from '@/i18n/routing'; +import { getPath } from '@/utils/page'; import { refreshPage } from '@/utils/refreshPage'; import { CustomError, handleServerResponse } from '@/utils/serverActionError'; -import { councilMinute } from '@/constants/segmentNode'; -import { getPath } from '@/utils/page'; - -const YEAR_LIMIT_COUNT = 10; -interface MinuteContent { - year: number; - index: number; - attachments: Attachment[]; -} - -const TIME_LINE_YEARS = [2025, 2024, 2023]; // temp +import CouncilAttachment from '../components/CouncilAttachments'; const minutePath = getPath(councilMinute); -export default function MinutePageContent({ contents }: { contents: MinuteContent[] }) { - const [selectedYear, setSelectedYear] = useState(contents[0].year); - // const timeLineYears = contents.map((change) => change.year).slice(0, YEAR_LIMIT_COUNT); - const timeLineYears = TIME_LINE_YEARS; - const yearLimit = timeLineYears.at(-1) ?? 0; - // const selectedContents = getSelectedContents(selectedYear, yearLimit, contents); - const selectedContents = contents; +export default function MinutePageContent({ + contents, +}: { + contents: { [year: string]: Minute[] }; +}) { + const [selectedYear, setSelectedYear] = useState(2025); + const timeLineYears = Object.keys(contents) + .map(Number) + .sort((a, b) => b - a); + const selectedContents = contents[selectedYear.toString()] ?? []; const pathname = usePathname(); - const handleDelete = async (year: number) => { - const index = 1; - - const resp = await deleteMinuteAction(year, index); - handleServerResponse(resp, { - successMessage: `${year}년 ${index}차 회의록을 삭제했습니다.`, - onSuccess: refreshPage, - }); - }; - - const getEditHref = (year: number) => `${pathname}/edit?year=${year}`; + // const getEditHref = (year: number) => `${pathname}/edit?year=${year}`; return ( @@ -57,7 +41,9 @@ export default function MinutePageContent({ contents }: { contents: MinuteConten setSelectedTime={setSelectedYear} />
- + {selectedContents.map((minute) => { + return ; + })}
); @@ -94,61 +80,27 @@ function Buttons({ ); } -function Minutes({ minute }: { minute: MinuteContent }) { +function Minutes({ minute }: { minute: Minute }) { + const handleDelete = async () => { + const resp = await deleteMinuteAction(minute.year, minute.index); + handleServerResponse(resp, { + successMessage: `${minute.year}년 ${minute.index}차 회의록을 삭제했습니다.`, + onSuccess: refreshPage, + }); + }; + return ( -
-
+
+
{minute.index}차 회의 회의록
deleteMinuteAction(minute.year, minute.index)} + onDelete={handleDelete} editHref={`${minutePath}/edit/${minute.year}/${minute.index}`} />
{minute.attachments.map((file) => ( - + ))}
); } - -function ContentViewer({ contents }: { contents: MinuteContent[] }) { - return ( -
- {contents.map((minute) => { - return ; - })} -
- ); -} - -function TogglableContentViewer({ - expandDefault = false, - contents, -}: { - expandDefault?: boolean; - contents: MinuteContent[]; -}) { - const [isExpanded, toggleContent] = useReducer((x) => !x, expandDefault); - - return ( -
- - {isExpanded && - contents.map((minute) => { - return ; - })} -
- ); -} - -const getSelectedContents = (year: number, yearLimit: number, data: MinuteContent[]) => { - if (year <= yearLimit) return data.filter((d) => d.year <= yearLimit); - - const change = data.find((d) => d.year === year); - return change ? [change] : [{ year, attachments: [] }]; -}; diff --git a/app/[locale]/community/council/minute/page.tsx b/app/[locale]/community/council/minute/page.tsx index f9a0d754..266ac628 100644 --- a/app/[locale]/community/council/minute/page.tsx +++ b/app/[locale]/community/council/minute/page.tsx @@ -1,12 +1,7 @@ -import { getCouncilMinutesByYear } from '@/apis/v2/council/meeting-minute'; +import { getCouncilMinutes } from '@/apis/v2/council/meeting-minute'; import { councilMinute } from '@/constants/segmentNode'; import { getMetadata } from '@/utils/metadata'; -import PageLayout from '@/components/layout/pageLayout/PageLayout'; -import LoginVisible from '@/components/common/LoginVisible'; -import { Link } from '@/i18n/routing'; -import { getPath } from '@/utils/page'; - import MinutePageContent from './MinutePageContent'; export async function generateMetadata(props: { params: Promise<{ locale: string }> }) { @@ -17,17 +12,9 @@ export async function generateMetadata(props: { params: Promise<{ locale: string return await getMetadata({ locale, node: councilMinute }); } -const path = getPath(councilMinute); - export default async function CouncilMinutePage() { - const data = await getCouncilMinutesByYear(2025); + const data = await getCouncilMinutes(); console.log(data); return ; - // return ( - // - // - //
hihi
- //
- // ); } From 245fbd0458cca910d738c83f11fa551eac188fb2 Mon Sep 17 00:00:00 2001 From: chansol Date: Tue, 25 Feb 2025 01:04:06 +0900 Subject: [PATCH 07/18] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9D=98=EB=A1=9D=20?= =?UTF-8?q?=ED=8E=B8=EC=A7=91=20=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- actions/council.ts | 3 + .../community/council/minute/MinuteEditor.tsx | 67 +++++++++++++++---- .../council/minute/MinutePageContent.tsx | 16 ++--- .../community/council/minute/create/page.tsx | 16 ++++- .../community/council/minute/edit/page.tsx | 34 ++++++++++ .../community/council/minute/page.tsx | 1 - utils/string.ts | 2 +- 7 files changed, 112 insertions(+), 27 deletions(-) create mode 100644 app/[locale]/community/council/minute/edit/page.tsx diff --git a/actions/council.ts b/actions/council.ts index feb1b6e4..5d67342d 100644 --- a/actions/council.ts +++ b/actions/council.ts @@ -17,6 +17,7 @@ import { import { councilIntro, councilMinute, councilReportList } from '@/constants/segmentNode'; import { redirectKo } from '@/i18n/routing'; import { getPath } from '@/utils/page'; +import { decodeFormDataFileName } from '@/utils/string'; import { withErrorHandler } from './errorHandler'; @@ -36,6 +37,7 @@ const minutePath = getPath(councilMinute); export const postMinutesByYearAction = withErrorHandler( async (year: number, formData: FormData) => { + decodeFormDataFileName(formData, 'attachments'); await postCouncilMinutesByYear(year, formData); revalidateTag(FETCH_TAG_COUNCIL_MINUTE); redirectKo(minutePath); @@ -44,6 +46,7 @@ export const postMinutesByYearAction = withErrorHandler( export const putMinuteAction = withErrorHandler( async (year: number, index: number, formData: FormData) => { + decodeFormDataFileName(formData, 'addFiles'); await putCouncilMinute(year, index, formData); revalidateTag(FETCH_TAG_COUNCIL_MINUTE); redirectKo(minutePath); diff --git a/app/[locale]/community/council/minute/MinuteEditor.tsx b/app/[locale]/community/council/minute/MinuteEditor.tsx index ee9035cf..5c6e5167 100644 --- a/app/[locale]/community/council/minute/MinuteEditor.tsx +++ b/app/[locale]/community/council/minute/MinuteEditor.tsx @@ -2,25 +2,33 @@ import { FormProvider, useForm } from 'react-hook-form'; +import { postMinutesByYearAction, putMinuteAction } from '@/actions/council'; import Fieldset from '@/components/form/Fieldset'; import Form from '@/components/form/Form'; import { useRouter } from '@/i18n/routing'; import { EditorFile, isLocalFile } from '@/types/form'; +import { getAttachmentDeleteIds } from '@/utils/formData'; import { handleServerResponse } from '@/utils/serverActionError'; import { encodeFormDataFileName } from '@/utils/string'; -import { postMinutesByYearAction } from '@/actions/council'; -export type MinutelineFormData = { year: number; file: EditorFile[] }; +export type MinuteFormData = { year: number; index: number; file: EditorFile[] }; interface Props { - defaultValues?: MinutelineFormData; - // onSubmit: (data: FormData) => Promise; + option: + | { + type: 'CREATE'; + defaultValues?: { year: number }; + } + | { + type: 'EDIT'; + defaultValues: MinuteFormData; + }; cancelPath: string; } -export default function MinuteEditor({ defaultValues, cancelPath }: Props) { - const formMethods = useForm({ - defaultValues: defaultValues ?? { +export default function MinuteEditor({ option, cancelPath }: Props) { + const formMethods = useForm({ + defaultValues: option.defaultValues ?? { year: new Date().getFullYear() + 1, file: [], }, @@ -30,11 +38,23 @@ export default function MinuteEditor({ defaultValues, cancelPath }: Props) { const router = useRouter(); const onCancel = () => router.push(cancelPath); - const onSubmit = async (requestObject: MinutelineFormData) => { - const formData = contentToFormData(requestObject.file); + const onSubmit = async (requestObject: MinuteFormData) => { + if (option.type === 'CREATE') { + const formData = contentToCreateFormData(requestObject.file); - const resp = await postMinutesByYearAction(requestObject.year, formData); - handleServerResponse(resp, { successMessage: '저장되었습니다.' }); + const resp = await postMinutesByYearAction(requestObject.year, formData); + handleServerResponse(resp, { successMessage: '저장되었습니다.' }); + } else { + const removeFileIds = getAttachmentDeleteIds(requestObject.file, option.defaultValues.file); + const formData = contentToEditFormData(removeFileIds, requestObject.file); + + const resp = await putMinuteAction( + option.defaultValues.year, + option.defaultValues.index, + formData, + ); + handleServerResponse(resp, { successMessage: '저장되었습니다.' }); + } }; return ( @@ -44,7 +64,7 @@ export default function MinuteEditor({ defaultValues, cancelPath }: Props) { @@ -57,7 +77,7 @@ export default function MinuteEditor({ defaultValues, cancelPath }: Props) { ); } -const contentToFormData = (attachments: EditorFile[]) => { +const contentToCreateFormData = (attachments: EditorFile[]) => { const formData = new FormData(); if (attachments) { @@ -70,3 +90,24 @@ const contentToFormData = (attachments: EditorFile[]) => { return formData; }; + +const contentToEditFormData = (removeFileIds: number[], addFiles: EditorFile[]) => { + const formData = new FormData(); + + formData.append( + 'removeFileIds', + new Blob([JSON.stringify(removeFileIds)], { + type: 'application/json', + }), + ); + + if (addFiles) { + encodeFormDataFileName( + formData, + 'addFiles', + addFiles.filter(isLocalFile).map((x) => x.file), + ); + } + + return formData; +}; diff --git a/app/[locale]/community/council/minute/MinutePageContent.tsx b/app/[locale]/community/council/minute/MinutePageContent.tsx index c69a876c..7e057c92 100644 --- a/app/[locale]/community/council/minute/MinutePageContent.tsx +++ b/app/[locale]/community/council/minute/MinutePageContent.tsx @@ -9,7 +9,7 @@ import { DeleteButton, EditButton } from '@/components/common/Buttons'; import LoginVisible from '@/components/common/LoginVisible'; import PageLayout from '@/components/layout/pageLayout/PageLayout'; import { councilMinute } from '@/constants/segmentNode'; -import { Link, usePathname } from '@/i18n/routing'; +import { Link } from '@/i18n/routing'; import { getPath } from '@/utils/page'; import { refreshPage } from '@/utils/refreshPage'; import { CustomError, handleServerResponse } from '@/utils/serverActionError'; @@ -17,24 +17,22 @@ import { CustomError, handleServerResponse } from '@/utils/serverActionError'; import CouncilAttachment from '../components/CouncilAttachments'; const minutePath = getPath(councilMinute); +const THIS_YEAR = new Date().getFullYear(); export default function MinutePageContent({ contents, }: { contents: { [year: string]: Minute[] }; }) { - const [selectedYear, setSelectedYear] = useState(2025); + const [selectedYear, setSelectedYear] = useState(THIS_YEAR); const timeLineYears = Object.keys(contents) .map(Number) .sort((a, b) => b - a); const selectedContents = contents[selectedYear.toString()] ?? []; - const pathname = usePathname(); - - // const getEditHref = (year: number) => `${pathname}/edit?year=${year}`; return ( - + add @@ -95,7 +93,7 @@ function Minutes({ minute }: { minute: Minute }) {
{minute.index}차 회의 회의록
{minute.attachments.map((file) => ( diff --git a/app/[locale]/community/council/minute/create/page.tsx b/app/[locale]/community/council/minute/create/page.tsx index f4b6ed57..af0e7818 100644 --- a/app/[locale]/community/council/minute/create/page.tsx +++ b/app/[locale]/community/council/minute/create/page.tsx @@ -1,14 +1,24 @@ import PageLayout from '@/components/layout/pageLayout/PageLayout'; import { councilMinute } from '@/constants/segmentNode'; import { getPath } from '@/utils/page'; + import MinuteEditor from '../MinuteEditor'; +interface MinuteCreatePageProps { + searchParams: Promise<{ year?: string }>; +} + const minutePath = getPath(councilMinute); -export default function CouncilMinuteCreatePage() { +export default async function CouncilMinuteCreatePage({ searchParams }: MinuteCreatePageProps) { + const { year } = await searchParams; + return ( - - + + ); } diff --git a/app/[locale]/community/council/minute/edit/page.tsx b/app/[locale]/community/council/minute/edit/page.tsx new file mode 100644 index 00000000..3491cd4f --- /dev/null +++ b/app/[locale]/community/council/minute/edit/page.tsx @@ -0,0 +1,34 @@ +import { getCouncilMinute } from '@/apis/v2/council/meeting-minute'; +import PageLayout from '@/components/layout/pageLayout/PageLayout'; +import { councilMinute } from '@/constants/segmentNode'; +import { getEditorFile } from '@/utils/formData'; +import { getPath } from '@/utils/page'; + +import MinuteEditor from '../MinuteEditor'; + +interface MinuteEditPageProps { + searchParams: Promise<{ year: string; index: string }>; +} + +const minutePath = getPath(councilMinute); + +export default async function CouncilMinuteEditPage({ searchParams }: MinuteEditPageProps) { + const year = Number((await searchParams).year); + const index = Number((await searchParams).index); + + const data = await getCouncilMinute(year, index); + + console.log(data); + + return ( + + + + ); +} diff --git a/app/[locale]/community/council/minute/page.tsx b/app/[locale]/community/council/minute/page.tsx index 266ac628..a2651020 100644 --- a/app/[locale]/community/council/minute/page.tsx +++ b/app/[locale]/community/council/minute/page.tsx @@ -14,7 +14,6 @@ export async function generateMetadata(props: { params: Promise<{ locale: string export default async function CouncilMinutePage() { const data = await getCouncilMinutes(); - console.log(data); return ; } diff --git a/utils/string.ts b/utils/string.ts index 30ada8fe..4376ef75 100644 --- a/utils/string.ts +++ b/utils/string.ts @@ -20,6 +20,6 @@ export const encodeFormDataFileName = ( fileList.forEach((file) => formData.append(key, file, encodeURI(file.name))); }; -type FormDataFileName = 'attachments' | 'newAttachments' | 'pdf'; +type FormDataFileName = 'attachments' | 'newAttachments' | 'pdf' | 'addFiles'; const isFile = (x: unknown): x is File => x instanceof File; From 009c5439822dbcc13f70edde6414721efafab8c4 Mon Sep 17 00:00:00 2001 From: chansol Date: Tue, 25 Feb 2025 01:26:33 +0900 Subject: [PATCH 08/18] =?UTF-8?q?feat:=20=EC=83=88=EB=A1=9C=EC=9A=B4=20?= =?UTF-8?q?=ED=9A=8C=EC=9D=98=EB=A1=9D=20=EC=B6=94=EA=B0=80=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../community/council/minute/MinuteEditor.tsx | 11 ++++++---- .../council/minute/MinutePageContent.tsx | 21 +++++++++++++++---- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/app/[locale]/community/council/minute/MinuteEditor.tsx b/app/[locale]/community/council/minute/MinuteEditor.tsx index 5c6e5167..2c16dacb 100644 --- a/app/[locale]/community/council/minute/MinuteEditor.tsx +++ b/app/[locale]/community/council/minute/MinuteEditor.tsx @@ -28,10 +28,13 @@ interface Props { export default function MinuteEditor({ option, cancelPath }: Props) { const formMethods = useForm({ - defaultValues: option.defaultValues ?? { - year: new Date().getFullYear() + 1, - file: [], - }, + defaultValues: + option.type === 'EDIT' + ? option.defaultValues + : { + year: option.defaultValues?.year ?? new Date().getFullYear() + 1, + file: [], + }, }); const { handleSubmit } = formMethods; diff --git a/app/[locale]/community/council/minute/MinutePageContent.tsx b/app/[locale]/community/council/minute/MinutePageContent.tsx index 7e057c92..b628b2ac 100644 --- a/app/[locale]/community/council/minute/MinutePageContent.tsx +++ b/app/[locale]/community/council/minute/MinutePageContent.tsx @@ -11,7 +11,6 @@ import PageLayout from '@/components/layout/pageLayout/PageLayout'; import { councilMinute } from '@/constants/segmentNode'; import { Link } from '@/i18n/routing'; import { getPath } from '@/utils/page'; -import { refreshPage } from '@/utils/refreshPage'; import { CustomError, handleServerResponse } from '@/utils/serverActionError'; import CouncilAttachment from '../components/CouncilAttachments'; @@ -32,7 +31,7 @@ export default function MinutePageContent({ return ( - + ; })}
+ ); } -function AddButton() { +function MinuteAddButton({ year }: { year: number }) { + return ( + + + add + 회의록 추가 + + + ); +} + +function YearAddButton() { return ( Date: Tue, 25 Feb 2025 16:45:43 +0900 Subject: [PATCH 09/18] =?UTF-8?q?fix:=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=EB=AA=85=20=EB=B3=80=EA=B2=BD=20=EB=93=B1=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apis/types/council.ts | 2 +- apis/v2/council/meeting-minute.ts | 10 +++++----- .../{minute => meeting-minute}/MinuteEditor.tsx | 2 +- .../{minute => meeting-minute}/MinutePageContent.tsx | 6 +++--- .../council/{minute => meeting-minute}/create/page.tsx | 5 +++-- .../council/{minute => meeting-minute}/edit/page.tsx | 6 ++---- .../council/{minute => meeting-minute}/page.tsx | 0 components/common/Attachments.tsx | 1 - constants/segmentNode.ts | 2 +- 9 files changed, 16 insertions(+), 18 deletions(-) rename app/[locale]/community/council/{minute => meeting-minute}/MinuteEditor.tsx (97%) rename app/[locale]/community/council/{minute => meeting-minute}/MinutePageContent.tsx (95%) rename app/[locale]/community/council/{minute => meeting-minute}/create/page.tsx (85%) rename app/[locale]/community/council/{minute => meeting-minute}/edit/page.tsx (91%) rename app/[locale]/community/council/{minute => meeting-minute}/page.tsx (100%) diff --git a/apis/types/council.ts b/apis/types/council.ts index 7973dbe9..b4f0834e 100644 --- a/apis/types/council.ts +++ b/apis/types/council.ts @@ -1,6 +1,6 @@ import { Attachment } from './attachment'; -export interface Minute { +export interface CouncilMeetingMinute { year: number; index: number; attachments: Attachment[]; diff --git a/apis/v2/council/meeting-minute.ts b/apis/v2/council/meeting-minute.ts index 319463f9..cac2960f 100644 --- a/apis/v2/council/meeting-minute.ts +++ b/apis/v2/council/meeting-minute.ts @@ -1,10 +1,10 @@ import { deleteRequest, postRequest, putRequest } from '@/apis'; import { getRequest } from '@/apis'; -import { Minute } from '@/apis/types/council'; +import { CouncilMeetingMinute } from '@/apis/types/council'; import { FETCH_TAG_COUNCIL_MINUTE } from '@/constants/network'; interface GETMinutesResponse { - [year: string]: Minute[]; + [year: string]: CouncilMeetingMinute[]; } export const getCouncilMinutes = () => @@ -13,7 +13,7 @@ export const getCouncilMinutes = () => }); export const getCouncilMinutesByYear = (year: number) => - getRequest(`/v2/council/meeting-minute/${year}`, undefined, { + getRequest(`/v2/council/meeting-minute/${year}`, undefined, { next: { tags: [FETCH_TAG_COUNCIL_MINUTE] }, }); @@ -21,12 +21,12 @@ export const postCouncilMinutesByYear = (year: number, formData: FormData) => postRequest(`/v2/council/meeting-minute/${year}`, { body: formData, jsessionID: true }); export const getCouncilMinute = (year: number, index: number) => - getRequest(`/v2/council/meeting-minute/${year}/${index}`, undefined, { + getRequest(`/v2/council/meeting-minute/${year}/${index}`, undefined, { next: { tags: [FETCH_TAG_COUNCIL_MINUTE] }, }); export const putCouncilMinute = (year: number, index: number, formData: FormData) => putRequest(`/v2/council/meeting-minute/${year}/${index}`, { body: formData, jsessionID: true }); -export const deleteCouncilMinute = async (year: number, index: number) => +export const deleteCouncilMinute = (year: number, index: number) => deleteRequest(`/v2/council/meeting-minute/${year}/${index}`, { jsessionID: true }); diff --git a/app/[locale]/community/council/minute/MinuteEditor.tsx b/app/[locale]/community/council/meeting-minute/MinuteEditor.tsx similarity index 97% rename from app/[locale]/community/council/minute/MinuteEditor.tsx rename to app/[locale]/community/council/meeting-minute/MinuteEditor.tsx index 2c16dacb..16bf4cbe 100644 --- a/app/[locale]/community/council/minute/MinuteEditor.tsx +++ b/app/[locale]/community/council/meeting-minute/MinuteEditor.tsx @@ -26,7 +26,7 @@ interface Props { cancelPath: string; } -export default function MinuteEditor({ option, cancelPath }: Props) { +export default function CouncilMeetingMinuteEditor({ option, cancelPath }: Props) { const formMethods = useForm({ defaultValues: option.type === 'EDIT' diff --git a/app/[locale]/community/council/minute/MinutePageContent.tsx b/app/[locale]/community/council/meeting-minute/MinutePageContent.tsx similarity index 95% rename from app/[locale]/community/council/minute/MinutePageContent.tsx rename to app/[locale]/community/council/meeting-minute/MinutePageContent.tsx index b628b2ac..f76b4798 100644 --- a/app/[locale]/community/council/minute/MinutePageContent.tsx +++ b/app/[locale]/community/council/meeting-minute/MinutePageContent.tsx @@ -3,7 +3,7 @@ import { useState } from 'react'; import { deleteMinuteAction } from '@/actions/council'; -import { Minute } from '@/apis/types/council'; +import { CouncilMeetingMinute } from '@/apis/types/council'; import Timeline from '@/app/[locale]/academics/components/timeline/Timeline'; import { DeleteButton, EditButton } from '@/components/common/Buttons'; import LoginVisible from '@/components/common/LoginVisible'; @@ -21,7 +21,7 @@ const THIS_YEAR = new Date().getFullYear(); export default function MinutePageContent({ contents, }: { - contents: { [year: string]: Minute[] }; + contents: { [year: string]: CouncilMeetingMinute[] }; }) { const [selectedYear, setSelectedYear] = useState(THIS_YEAR); const timeLineYears = Object.keys(contents) @@ -92,7 +92,7 @@ function Buttons({ ); } -function Minutes({ minute }: { minute: Minute }) { +function Minutes({ minute }: { minute: CouncilMeetingMinute }) { const handleDelete = async () => { const resp = await deleteMinuteAction(minute.year, minute.index); handleServerResponse(resp, { diff --git a/app/[locale]/community/council/minute/create/page.tsx b/app/[locale]/community/council/meeting-minute/create/page.tsx similarity index 85% rename from app/[locale]/community/council/minute/create/page.tsx rename to app/[locale]/community/council/meeting-minute/create/page.tsx index af0e7818..6673766c 100644 --- a/app/[locale]/community/council/minute/create/page.tsx +++ b/app/[locale]/community/council/meeting-minute/create/page.tsx @@ -2,7 +2,7 @@ import PageLayout from '@/components/layout/pageLayout/PageLayout'; import { councilMinute } from '@/constants/segmentNode'; import { getPath } from '@/utils/page'; -import MinuteEditor from '../MinuteEditor'; +import CouncilMeetingMinuteEditor from '../MinuteEditor'; interface MinuteCreatePageProps { searchParams: Promise<{ year?: string }>; @@ -14,8 +14,9 @@ export default async function CouncilMinuteCreatePage({ searchParams }: MinuteCr const { year } = await searchParams; return ( + // TODO: 영문 번역 - diff --git a/app/[locale]/community/council/minute/edit/page.tsx b/app/[locale]/community/council/meeting-minute/edit/page.tsx similarity index 91% rename from app/[locale]/community/council/minute/edit/page.tsx rename to app/[locale]/community/council/meeting-minute/edit/page.tsx index 3491cd4f..ef8e8c4a 100644 --- a/app/[locale]/community/council/minute/edit/page.tsx +++ b/app/[locale]/community/council/meeting-minute/edit/page.tsx @@ -4,7 +4,7 @@ import { councilMinute } from '@/constants/segmentNode'; import { getEditorFile } from '@/utils/formData'; import { getPath } from '@/utils/page'; -import MinuteEditor from '../MinuteEditor'; +import CouncilMeetingMinuteEditor from '../MinuteEditor'; interface MinuteEditPageProps { searchParams: Promise<{ year: string; index: string }>; @@ -18,11 +18,9 @@ export default async function CouncilMinuteEditPage({ searchParams }: MinuteEdit const data = await getCouncilMinute(year, index); - console.log(data); - return ( - Date: Tue, 25 Feb 2025 16:55:23 +0900 Subject: [PATCH 10/18] =?UTF-8?q?fix:=20year=20index=20NaN=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../community/council/meeting-minute/create/page.tsx | 9 ++++++--- .../community/council/meeting-minute/edit/page.tsx | 11 ++++++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/app/[locale]/community/council/meeting-minute/create/page.tsx b/app/[locale]/community/council/meeting-minute/create/page.tsx index 6673766c..1eff62e0 100644 --- a/app/[locale]/community/council/meeting-minute/create/page.tsx +++ b/app/[locale]/community/council/meeting-minute/create/page.tsx @@ -10,14 +10,17 @@ interface MinuteCreatePageProps { const minutePath = getPath(councilMinute); -export default async function CouncilMinuteCreatePage({ searchParams }: MinuteCreatePageProps) { - const { year } = await searchParams; +export default async function CouncilMinuteCreatePage(props: MinuteCreatePageProps) { + const searchParams = await props.searchParams; + + const year = Number(searchParams.year); + if (Number.isNaN(year)) throw new Error('/meeting-minute?year=[year]: year가 숫자가 아닙니다.'); return ( // TODO: 영문 번역 diff --git a/app/[locale]/community/council/meeting-minute/edit/page.tsx b/app/[locale]/community/council/meeting-minute/edit/page.tsx index ef8e8c4a..cb23ebf2 100644 --- a/app/[locale]/community/council/meeting-minute/edit/page.tsx +++ b/app/[locale]/community/council/meeting-minute/edit/page.tsx @@ -12,13 +12,18 @@ interface MinuteEditPageProps { const minutePath = getPath(councilMinute); -export default async function CouncilMinuteEditPage({ searchParams }: MinuteEditPageProps) { - const year = Number((await searchParams).year); - const index = Number((await searchParams).index); +export default async function CouncilMinuteEditPage(props: MinuteEditPageProps) { + const searchParams = await props.searchParams; + const year = Number(searchParams.year); + const index = Number(searchParams.index); + + if (Number.isNaN(year) || Number.isNaN(index)) + throw new Error('/meeting-minute?year=[year]&index=[index]: year나 index가 숫자가 아닙니다.'); const data = await getCouncilMinute(year, index); return ( + // TODO: 영문 번역 Date: Tue, 25 Feb 2025 17:23:05 +0900 Subject: [PATCH 11/18] =?UTF-8?q?fix:=20contentToFormData=20=ED=99=95?= =?UTF-8?q?=EC=9E=A5=ED=95=B4=EC=84=9C=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...tor.tsx => CouncilMeetingMinuteEditor.tsx} | 50 ++++--------------- .../council/meeting-minute/create/page.tsx | 2 +- .../council/meeting-minute/edit/page.tsx | 2 +- utils/formData.ts | 25 ++++++---- 4 files changed, 27 insertions(+), 52 deletions(-) rename app/[locale]/community/council/meeting-minute/{MinuteEditor.tsx => CouncilMeetingMinuteEditor.tsx} (66%) diff --git a/app/[locale]/community/council/meeting-minute/MinuteEditor.tsx b/app/[locale]/community/council/meeting-minute/CouncilMeetingMinuteEditor.tsx similarity index 66% rename from app/[locale]/community/council/meeting-minute/MinuteEditor.tsx rename to app/[locale]/community/council/meeting-minute/CouncilMeetingMinuteEditor.tsx index 16bf4cbe..642734e2 100644 --- a/app/[locale]/community/council/meeting-minute/MinuteEditor.tsx +++ b/app/[locale]/community/council/meeting-minute/CouncilMeetingMinuteEditor.tsx @@ -6,12 +6,11 @@ import { postMinutesByYearAction, putMinuteAction } from '@/actions/council'; import Fieldset from '@/components/form/Fieldset'; import Form from '@/components/form/Form'; import { useRouter } from '@/i18n/routing'; -import { EditorFile, isLocalFile } from '@/types/form'; -import { getAttachmentDeleteIds } from '@/utils/formData'; +import { EditorFile } from '@/types/form'; +import { contentToFormData, getAttachmentDeleteIds } from '@/utils/formData'; import { handleServerResponse } from '@/utils/serverActionError'; -import { encodeFormDataFileName } from '@/utils/string'; -export type MinuteFormData = { year: number; index: number; file: EditorFile[] }; +type MinuteFormData = { year: number; index: number; file: EditorFile[] }; interface Props { option: @@ -43,13 +42,17 @@ export default function CouncilMeetingMinuteEditor({ option, cancelPath }: Props const onSubmit = async (requestObject: MinuteFormData) => { if (option.type === 'CREATE') { - const formData = contentToCreateFormData(requestObject.file); + const formData = contentToFormData(option.type, { attachments: requestObject.file }); const resp = await postMinutesByYearAction(requestObject.year, formData); handleServerResponse(resp, { successMessage: '저장되었습니다.' }); } else { const removeFileIds = getAttachmentDeleteIds(requestObject.file, option.defaultValues.file); - const formData = contentToEditFormData(removeFileIds, requestObject.file); + const formData = contentToFormData( + 'EDIT', + { requestObject: removeFileIds, attachments: requestObject.file }, + { request: 'removeFileIdds', attachments: 'addFiles' }, + ); const resp = await putMinuteAction( option.defaultValues.year, @@ -79,38 +82,3 @@ export default function CouncilMeetingMinuteEditor({ option, cancelPath }: Props ); } - -const contentToCreateFormData = (attachments: EditorFile[]) => { - const formData = new FormData(); - - if (attachments) { - encodeFormDataFileName( - formData, - 'attachments', - attachments.filter(isLocalFile).map((x) => x.file), - ); - } - - return formData; -}; - -const contentToEditFormData = (removeFileIds: number[], addFiles: EditorFile[]) => { - const formData = new FormData(); - - formData.append( - 'removeFileIds', - new Blob([JSON.stringify(removeFileIds)], { - type: 'application/json', - }), - ); - - if (addFiles) { - encodeFormDataFileName( - formData, - 'addFiles', - addFiles.filter(isLocalFile).map((x) => x.file), - ); - } - - return formData; -}; diff --git a/app/[locale]/community/council/meeting-minute/create/page.tsx b/app/[locale]/community/council/meeting-minute/create/page.tsx index 1eff62e0..05061c60 100644 --- a/app/[locale]/community/council/meeting-minute/create/page.tsx +++ b/app/[locale]/community/council/meeting-minute/create/page.tsx @@ -2,7 +2,7 @@ import PageLayout from '@/components/layout/pageLayout/PageLayout'; import { councilMinute } from '@/constants/segmentNode'; import { getPath } from '@/utils/page'; -import CouncilMeetingMinuteEditor from '../MinuteEditor'; +import CouncilMeetingMinuteEditor from '../CouncilMeetingMinuteEditor'; interface MinuteCreatePageProps { searchParams: Promise<{ year?: string }>; diff --git a/app/[locale]/community/council/meeting-minute/edit/page.tsx b/app/[locale]/community/council/meeting-minute/edit/page.tsx index cb23ebf2..3870116b 100644 --- a/app/[locale]/community/council/meeting-minute/edit/page.tsx +++ b/app/[locale]/community/council/meeting-minute/edit/page.tsx @@ -4,7 +4,7 @@ import { councilMinute } from '@/constants/segmentNode'; import { getEditorFile } from '@/utils/formData'; import { getPath } from '@/utils/page'; -import CouncilMeetingMinuteEditor from '../MinuteEditor'; +import CouncilMeetingMinuteEditor from '../CouncilMeetingMinuteEditor'; interface MinuteEditPageProps { searchParams: Promise<{ year: string; index: string }>; diff --git a/utils/formData.ts b/utils/formData.ts index f7505f8f..a553af3e 100644 --- a/utils/formData.ts +++ b/utils/formData.ts @@ -6,31 +6,38 @@ import { encodeFormDataFileName } from './string'; export const contentToFormData = ( type: 'CREATE' | 'EDIT', content: { - requestObject: object; + requestObject?: object; attachments?: EditorFile[]; image?: EditorImage; }, + keys?: { + request?: string; + attachments?: string; + image?: string; + }, ) => { const { requestObject, attachments, image } = content; const formData = new FormData(); - formData.append( - 'request', - new Blob([JSON.stringify(requestObject)], { - type: 'application/json', - }), - ); + if (requestObject) { + formData.append( + keys?.request || 'request', + new Blob([JSON.stringify(requestObject)], { + type: 'application/json', + }), + ); + } if (attachments) { encodeFormDataFileName( formData, - type === 'CREATE' ? 'attachments' : 'newAttachments', + keys?.attachments || type === 'CREATE' ? 'attachments' : 'newAttachments', attachments.filter(isLocalFile).map((x) => x.file), ); } if (image && isLocalImage(image)) { - formData.append(type === 'CREATE' ? 'mainImage' : 'newMainImage', image.file); + formData.append(keys?.image || type === 'CREATE' ? 'mainImage' : 'newMainImage', image.file); } return formData; From 32d8bf4ab8fa2c7e08cf662494c863206a8bc3e7 Mon Sep 17 00:00:00 2001 From: chansol Date: Tue, 25 Feb 2025 18:09:17 +0900 Subject: [PATCH 12/18] =?UTF-8?q?fix:=20MinuteEditor=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CouncilMeetingMinuteEditor.tsx | 62 +++++-------------- .../council/meeting-minute/create/page.tsx | 36 +++++++---- .../edit/EditMinutePageContent.tsx | 48 ++++++++++++++ .../council/meeting-minute/edit/page.tsx | 21 +------ 4 files changed, 91 insertions(+), 76 deletions(-) create mode 100644 app/[locale]/community/council/meeting-minute/edit/EditMinutePageContent.tsx diff --git a/app/[locale]/community/council/meeting-minute/CouncilMeetingMinuteEditor.tsx b/app/[locale]/community/council/meeting-minute/CouncilMeetingMinuteEditor.tsx index 642734e2..6390023a 100644 --- a/app/[locale]/community/council/meeting-minute/CouncilMeetingMinuteEditor.tsx +++ b/app/[locale]/community/council/meeting-minute/CouncilMeetingMinuteEditor.tsx @@ -2,65 +2,35 @@ import { FormProvider, useForm } from 'react-hook-form'; -import { postMinutesByYearAction, putMinuteAction } from '@/actions/council'; import Fieldset from '@/components/form/Fieldset'; import Form from '@/components/form/Form'; -import { useRouter } from '@/i18n/routing'; import { EditorFile } from '@/types/form'; -import { contentToFormData, getAttachmentDeleteIds } from '@/utils/formData'; import { handleServerResponse } from '@/utils/serverActionError'; -type MinuteFormData = { year: number; index: number; file: EditorFile[] }; +export type MinuteFormData = { year: number; file: EditorFile[] }; interface Props { - option: - | { - type: 'CREATE'; - defaultValues?: { year: number }; - } - | { - type: 'EDIT'; - defaultValues: MinuteFormData; - }; - cancelPath: string; + defaultValues?: MinuteFormData; + onSubmit: (formData: MinuteFormData) => Promise; + onCancel: () => void; } -export default function CouncilMeetingMinuteEditor({ option, cancelPath }: Props) { +export default function CouncilMeetingMinuteEditor({ + defaultValues, + onSubmit: _onSubmit, + onCancel, +}: Props) { const formMethods = useForm({ - defaultValues: - option.type === 'EDIT' - ? option.defaultValues - : { - year: option.defaultValues?.year ?? new Date().getFullYear() + 1, - file: [], - }, + defaultValues: defaultValues ?? { + year: new Date().getFullYear() + 1, + file: [], + }, }); const { handleSubmit } = formMethods; - const router = useRouter(); - const onCancel = () => router.push(cancelPath); - const onSubmit = async (requestObject: MinuteFormData) => { - if (option.type === 'CREATE') { - const formData = contentToFormData(option.type, { attachments: requestObject.file }); - - const resp = await postMinutesByYearAction(requestObject.year, formData); - handleServerResponse(resp, { successMessage: '저장되었습니다.' }); - } else { - const removeFileIds = getAttachmentDeleteIds(requestObject.file, option.defaultValues.file); - const formData = contentToFormData( - 'EDIT', - { requestObject: removeFileIds, attachments: requestObject.file }, - { request: 'removeFileIdds', attachments: 'addFiles' }, - ); - - const resp = await putMinuteAction( - option.defaultValues.year, - option.defaultValues.index, - formData, - ); - handleServerResponse(resp, { successMessage: '저장되었습니다.' }); - } + const resp = await _onSubmit(requestObject); + handleServerResponse(resp, { successMessage: '저장되었습니다.' }); }; return ( @@ -70,7 +40,7 @@ export default function CouncilMeetingMinuteEditor({ option, cancelPath }: Props diff --git a/app/[locale]/community/council/meeting-minute/create/page.tsx b/app/[locale]/community/council/meeting-minute/create/page.tsx index 05061c60..2cc7b6f3 100644 --- a/app/[locale]/community/council/meeting-minute/create/page.tsx +++ b/app/[locale]/community/council/meeting-minute/create/page.tsx @@ -1,27 +1,41 @@ +'use client'; + +import { postMinutesByYearAction } from '@/actions/council'; import PageLayout from '@/components/layout/pageLayout/PageLayout'; import { councilMinute } from '@/constants/segmentNode'; +import { useRouter } from '@/i18n/routing'; +import { contentToFormData } from '@/utils/formData'; import { getPath } from '@/utils/page'; -import CouncilMeetingMinuteEditor from '../CouncilMeetingMinuteEditor'; - -interface MinuteCreatePageProps { - searchParams: Promise<{ year?: string }>; -} +import CouncilMeetingMinuteEditor, { MinuteFormData } from '../CouncilMeetingMinuteEditor'; const minutePath = getPath(councilMinute); -export default async function CouncilMinuteCreatePage(props: MinuteCreatePageProps) { - const searchParams = await props.searchParams; - +export default function CouncilMinuteCreatePage({ + searchParams, +}: { + searchParams: { year?: string }; +}) { const year = Number(searchParams.year); - if (Number.isNaN(year)) throw new Error('/meeting-minute?year=[year]: year가 숫자가 아닙니다.'); + if (searchParams.year !== undefined && Number.isNaN(year)) + throw new Error('/meeting-minute?year=[year]: year가 숫자가 아닙니다.'); + + const router = useRouter(); + + const onCancel = () => router.push(minutePath); + + const onSubmit = async (requestObject: MinuteFormData) => { + const formData = contentToFormData('CREATE', { attachments: requestObject.file }); + await postMinutesByYearAction(requestObject.year, formData); + }; return ( // TODO: 영문 번역 ); diff --git a/app/[locale]/community/council/meeting-minute/edit/EditMinutePageContent.tsx b/app/[locale]/community/council/meeting-minute/edit/EditMinutePageContent.tsx new file mode 100644 index 00000000..d031490d --- /dev/null +++ b/app/[locale]/community/council/meeting-minute/edit/EditMinutePageContent.tsx @@ -0,0 +1,48 @@ +'use client'; + +import { putMinuteAction } from '@/actions/council'; +import { CouncilMeetingMinute } from '@/apis/types/council'; +import PageLayout from '@/components/layout/pageLayout/PageLayout'; +import { councilMinute } from '@/constants/segmentNode'; +import { useRouter } from '@/i18n/routing'; +import { contentToFormData, getAttachmentDeleteIds, getEditorFile } from '@/utils/formData'; +import { getPath } from '@/utils/page'; + +import CouncilMeetingMinuteEditor, { MinuteFormData } from '../CouncilMeetingMinuteEditor'; + +const minutePath = getPath(councilMinute); + +export default function EditMinutePageContent({ + year, + index, + data, +}: { + year: number; + index: number; + data: CouncilMeetingMinute; +}) { + const router = useRouter(); + + const onCancel = () => router.push(minutePath); + + const onSubmit = async (requestObject: MinuteFormData) => { + const removeFileIds = getAttachmentDeleteIds(requestObject.file, data.attachments); + const formData = contentToFormData( + 'EDIT', + { requestObject: removeFileIds, attachments: requestObject.file }, + { request: 'removeFileIds', attachments: 'addFiles' }, + ); + await putMinuteAction(year, index, formData); + }; + + return ( + // TODO: 영문 번역 + + + + ); +} diff --git a/app/[locale]/community/council/meeting-minute/edit/page.tsx b/app/[locale]/community/council/meeting-minute/edit/page.tsx index 3870116b..3744a391 100644 --- a/app/[locale]/community/council/meeting-minute/edit/page.tsx +++ b/app/[locale]/community/council/meeting-minute/edit/page.tsx @@ -1,17 +1,11 @@ import { getCouncilMinute } from '@/apis/v2/council/meeting-minute'; -import PageLayout from '@/components/layout/pageLayout/PageLayout'; -import { councilMinute } from '@/constants/segmentNode'; -import { getEditorFile } from '@/utils/formData'; -import { getPath } from '@/utils/page'; -import CouncilMeetingMinuteEditor from '../CouncilMeetingMinuteEditor'; +import EditMinutePageContent from './EditMinutePageContent'; interface MinuteEditPageProps { searchParams: Promise<{ year: string; index: string }>; } -const minutePath = getPath(councilMinute); - export default async function CouncilMinuteEditPage(props: MinuteEditPageProps) { const searchParams = await props.searchParams; const year = Number(searchParams.year); @@ -22,16 +16,5 @@ export default async function CouncilMinuteEditPage(props: MinuteEditPageProps) const data = await getCouncilMinute(year, index); - return ( - // TODO: 영문 번역 - - - - ); + return ; } From 30c377ae373be669db8ae5e66c520785cad0b51d Mon Sep 17 00:00:00 2001 From: chansol Date: Tue, 25 Feb 2025 18:18:01 +0900 Subject: [PATCH 13/18] =?UTF-8?q?fix:=20=ED=95=99=EC=83=9D=ED=9A=8C=20?= =?UTF-8?q?=ED=8E=B8=EC=A7=91=20=EA=B6=8C=ED=95=9C=20council=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../community/council/meeting-minute/MinutePageContent.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/[locale]/community/council/meeting-minute/MinutePageContent.tsx b/app/[locale]/community/council/meeting-minute/MinutePageContent.tsx index f76b4798..345e4e4b 100644 --- a/app/[locale]/community/council/meeting-minute/MinutePageContent.tsx +++ b/app/[locale]/community/council/meeting-minute/MinutePageContent.tsx @@ -49,7 +49,7 @@ export default function MinutePageContent({ function MinuteAddButton({ year }: { year: number }) { return ( - + + +
From d8bc90341d38215134ba01f9ab2ff948fcdec495 Mon Sep 17 00:00:00 2001 From: chansol Date: Wed, 26 Feb 2025 13:45:27 +0900 Subject: [PATCH 14/18] =?UTF-8?q?fix:=20middleware=20=ED=95=99=EC=83=9D?= =?UTF-8?q?=ED=9A=8C=20=EA=B6=8C=ED=95=9C=20=ED=97=88=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../meeting-minute/MinutePageContent.tsx | 6 ++-- middleware.ts | 28 +++++++++++++------ 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/app/[locale]/community/council/meeting-minute/MinutePageContent.tsx b/app/[locale]/community/council/meeting-minute/MinutePageContent.tsx index 345e4e4b..acaef956 100644 --- a/app/[locale]/community/council/meeting-minute/MinutePageContent.tsx +++ b/app/[locale]/community/council/meeting-minute/MinutePageContent.tsx @@ -49,7 +49,7 @@ export default function MinutePageContent({ function MinuteAddButton({ year }: { year: number }) { return ( - + + +
diff --git a/middleware.ts b/middleware.ts index 574f9114..f9ca1e30 100644 --- a/middleware.ts +++ b/middleware.ts @@ -14,18 +14,28 @@ const isAuthRequired = (pathname: string) => { return pathname.startsWith('/admin') || pathname.endsWith('create') || pathname.endsWith('edit'); }; +const isCouncilRequired = (pathname: string) => { + if (pathname.startsWith('/en')) pathname = pathname.slice(3); + return ( + pathname.includes('/council') && (pathname.endsWith('create') || pathname.endsWith('edit')) + ); +}; + export default async function middleware(request: NextRequest) { const pathname = request.nextUrl.pathname; - + const userState = await getUserState(); // 관리자 페이지는 스태프 계정으로 로그인되어있어야한다. - if (isAuthRequired(pathname)) { - const isStaff = await getUserState(); - if (isStaff !== 'ROLE_STAFF') { - if (isProd) { - return Response.redirect(new URL(LOGIN_URL)); - } else { - return Response.redirect(new URL(BASE_URL)); - } + // 학생회 편집 페이지는 학생회 혹은 스태프 계정으로 로그인되어 있어야 한다. + const isValidState = + userState === 'ROLE_STAFF' || + (isCouncilRequired(pathname) && userState === 'ROLE_COUNCIL') || + !isAuthRequired(pathname); + + if (!isValidState) { + if (isProd) { + return Response.redirect(new URL(LOGIN_URL)); + } else { + return Response.redirect(new URL(BASE_URL)); } } From 83ca9e5df144aa8279f71d009401529a617c1976 Mon Sep 17 00:00:00 2001 From: chansol Date: Wed, 26 Feb 2025 15:20:43 +0900 Subject: [PATCH 15/18] =?UTF-8?q?fix:=20=ED=95=99=EC=83=9D=ED=9A=8C=20?= =?UTF-8?q?=ED=8E=B8=EC=A7=91=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=84=9C?= =?UTF-8?q?=EB=B8=8C=EB=82=B4=EB=B9=84=20=EC=88=A8=EA=B9=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/[locale]/community/council/meeting-minute/create/page.tsx | 2 +- .../council/meeting-minute/edit/EditMinutePageContent.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/[locale]/community/council/meeting-minute/create/page.tsx b/app/[locale]/community/council/meeting-minute/create/page.tsx index 2cc7b6f3..56024a0c 100644 --- a/app/[locale]/community/council/meeting-minute/create/page.tsx +++ b/app/[locale]/community/council/meeting-minute/create/page.tsx @@ -31,7 +31,7 @@ export default function CouncilMinuteCreatePage({ return ( // TODO: 영문 번역 - + + Date: Tue, 4 Mar 2025 17:19:05 +0900 Subject: [PATCH 16/18] =?UTF-8?q?fix:=20=ED=9A=8C=EC=9D=98=EB=A1=9D=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=EB=8A=94=20=EB=A7=88=EC=A7=80=EB=A7=89=20?= =?UTF-8?q?=ED=9A=8C=EC=B0=A8=EB=A7=8C=20=EA=B0=80=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../meeting-minute/MinutePageContent.tsx | 41 ++++++++----------- .../edit/EditMinutePageContent.tsx | 7 ++-- utils/formData.ts | 7 ++-- utils/string.ts | 2 +- 4 files changed, 25 insertions(+), 32 deletions(-) diff --git a/app/[locale]/community/council/meeting-minute/MinutePageContent.tsx b/app/[locale]/community/council/meeting-minute/MinutePageContent.tsx index acaef956..995688ad 100644 --- a/app/[locale]/community/council/meeting-minute/MinutePageContent.tsx +++ b/app/[locale]/community/council/meeting-minute/MinutePageContent.tsx @@ -11,7 +11,7 @@ import PageLayout from '@/components/layout/pageLayout/PageLayout'; import { councilMinute } from '@/constants/segmentNode'; import { Link } from '@/i18n/routing'; import { getPath } from '@/utils/page'; -import { CustomError, handleServerResponse } from '@/utils/serverActionError'; +import { handleServerResponse } from '@/utils/serverActionError'; import CouncilAttachment from '../components/CouncilAttachments'; @@ -38,8 +38,14 @@ export default function MinutePageContent({ setSelectedTime={setSelectedYear} />
- {selectedContents.map((minute) => { - return ; + {selectedContents.map((minute, i) => { + return ( + + ); })}
@@ -75,24 +81,7 @@ function YearAddButton() { ); } -function Buttons({ - onDelete, - editHref, -}: { - onDelete: () => Promise; - editHref: string; -}) { - return ( - -
- - -
-
- ); -} - -function Minutes({ minute }: { minute: CouncilMeetingMinute }) { +function Minutes({ minute, isLast }: { minute: CouncilMeetingMinute; isLast: boolean }) { const handleDelete = async () => { const resp = await deleteMinuteAction(minute.year, minute.index); handleServerResponse(resp, { @@ -104,10 +93,12 @@ function Minutes({ minute }: { minute: CouncilMeetingMinute }) {
{minute.index}차 회의 회의록
- + +
+ {isLast && } + +
+
{minute.attachments.map((file) => ( diff --git a/app/[locale]/community/council/meeting-minute/edit/EditMinutePageContent.tsx b/app/[locale]/community/council/meeting-minute/edit/EditMinutePageContent.tsx index df9d4051..37fab16e 100644 --- a/app/[locale]/community/council/meeting-minute/edit/EditMinutePageContent.tsx +++ b/app/[locale]/community/council/meeting-minute/edit/EditMinutePageContent.tsx @@ -26,12 +26,13 @@ export default function EditMinutePageContent({ const onCancel = () => router.push(minutePath); const onSubmit = async (requestObject: MinuteFormData) => { - const removeFileIds = getAttachmentDeleteIds(requestObject.file, data.attachments); + const deleteIds = getAttachmentDeleteIds(requestObject.file, data.attachments); const formData = contentToFormData( 'EDIT', - { requestObject: removeFileIds, attachments: requestObject.file }, - { request: 'removeFileIds', attachments: 'addFiles' }, + { requestObject: deleteIds, attachments: requestObject.file }, + { request: 'deleteIds', attachments: 'addFiles' }, ); + await putMinuteAction(year, index, formData); }; diff --git a/utils/formData.ts b/utils/formData.ts index a553af3e..af3ebd7a 100644 --- a/utils/formData.ts +++ b/utils/formData.ts @@ -1,7 +1,7 @@ import { Attachment } from '@/apis/types/attachment'; import { EditorFile, EditorImage, isLocalFile, isLocalImage, isUploadedFile } from '@/types/form'; -import { encodeFormDataFileName } from './string'; +import { encodeFormDataFileName, FormDataFileName } from './string'; export const contentToFormData = ( type: 'CREATE' | 'EDIT', @@ -31,13 +31,14 @@ export const contentToFormData = ( if (attachments) { encodeFormDataFileName( formData, - keys?.attachments || type === 'CREATE' ? 'attachments' : 'newAttachments', + (keys?.attachments as FormDataFileName) || + (type === 'CREATE' ? 'attachments' : 'newAttachments'), attachments.filter(isLocalFile).map((x) => x.file), ); } if (image && isLocalImage(image)) { - formData.append(keys?.image || type === 'CREATE' ? 'mainImage' : 'newMainImage', image.file); + formData.append(keys?.image || (type === 'CREATE' ? 'mainImage' : 'newMainImage'), image.file); } return formData; diff --git a/utils/string.ts b/utils/string.ts index 4376ef75..1d68612b 100644 --- a/utils/string.ts +++ b/utils/string.ts @@ -20,6 +20,6 @@ export const encodeFormDataFileName = ( fileList.forEach((file) => formData.append(key, file, encodeURI(file.name))); }; -type FormDataFileName = 'attachments' | 'newAttachments' | 'pdf' | 'addFiles'; +export type FormDataFileName = 'attachments' | 'newAttachments' | 'pdf' | 'addFiles'; const isFile = (x: unknown): x is File => x instanceof File; From d35d1129910f31ee0b3510a5c8e1508f72bd8d4f Mon Sep 17 00:00:00 2001 From: chansol Date: Wed, 5 Mar 2025 14:22:59 +0900 Subject: [PATCH 17/18] =?UTF-8?q?fix:=20council=20PUT=20api=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=EB=AA=85=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../meeting-minute/edit/EditMinutePageContent.tsx | 9 ++++----- utils/formData.ts | 14 ++++---------- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/app/[locale]/community/council/meeting-minute/edit/EditMinutePageContent.tsx b/app/[locale]/community/council/meeting-minute/edit/EditMinutePageContent.tsx index 37fab16e..4cf0ec58 100644 --- a/app/[locale]/community/council/meeting-minute/edit/EditMinutePageContent.tsx +++ b/app/[locale]/community/council/meeting-minute/edit/EditMinutePageContent.tsx @@ -27,11 +27,10 @@ export default function EditMinutePageContent({ const onSubmit = async (requestObject: MinuteFormData) => { const deleteIds = getAttachmentDeleteIds(requestObject.file, data.attachments); - const formData = contentToFormData( - 'EDIT', - { requestObject: deleteIds, attachments: requestObject.file }, - { request: 'deleteIds', attachments: 'addFiles' }, - ); + const formData = contentToFormData('EDIT', { + requestObject: { deleteIds }, + attachments: requestObject.file, + }); await putMinuteAction(year, index, formData); }; diff --git a/utils/formData.ts b/utils/formData.ts index af3ebd7a..0eb00c17 100644 --- a/utils/formData.ts +++ b/utils/formData.ts @@ -1,7 +1,7 @@ import { Attachment } from '@/apis/types/attachment'; import { EditorFile, EditorImage, isLocalFile, isLocalImage, isUploadedFile } from '@/types/form'; -import { encodeFormDataFileName, FormDataFileName } from './string'; +import { encodeFormDataFileName } from './string'; export const contentToFormData = ( type: 'CREATE' | 'EDIT', @@ -10,18 +10,13 @@ export const contentToFormData = ( attachments?: EditorFile[]; image?: EditorImage; }, - keys?: { - request?: string; - attachments?: string; - image?: string; - }, ) => { const { requestObject, attachments, image } = content; const formData = new FormData(); if (requestObject) { formData.append( - keys?.request || 'request', + 'request', new Blob([JSON.stringify(requestObject)], { type: 'application/json', }), @@ -31,14 +26,13 @@ export const contentToFormData = ( if (attachments) { encodeFormDataFileName( formData, - (keys?.attachments as FormDataFileName) || - (type === 'CREATE' ? 'attachments' : 'newAttachments'), + type === 'CREATE' ? 'attachments' : 'newAttachments', attachments.filter(isLocalFile).map((x) => x.file), ); } if (image && isLocalImage(image)) { - formData.append(keys?.image || (type === 'CREATE' ? 'mainImage' : 'newMainImage'), image.file); + formData.append(type === 'CREATE' ? 'mainImage' : 'newMainImage', image.file); } return formData; From 6e0869744b359cdf147d8579ac65f0b9bfc28ee7 Mon Sep 17 00:00:00 2001 From: chansol Date: Fri, 7 Mar 2025 14:39:25 +0900 Subject: [PATCH 18/18] =?UTF-8?q?fix:=20=EC=B2=A8=EB=B6=80=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EB=94=94=EC=BD=94=EB=94=A9=20=ED=82=A4=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- actions/council.ts | 2 +- utils/string.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/actions/council.ts b/actions/council.ts index 5d67342d..9dca47ee 100644 --- a/actions/council.ts +++ b/actions/council.ts @@ -46,7 +46,7 @@ export const postMinutesByYearAction = withErrorHandler( export const putMinuteAction = withErrorHandler( async (year: number, index: number, formData: FormData) => { - decodeFormDataFileName(formData, 'addFiles'); + decodeFormDataFileName(formData, 'newAttachments'); await putCouncilMinute(year, index, formData); revalidateTag(FETCH_TAG_COUNCIL_MINUTE); redirectKo(minutePath); diff --git a/utils/string.ts b/utils/string.ts index 1d68612b..3012b959 100644 --- a/utils/string.ts +++ b/utils/string.ts @@ -20,6 +20,6 @@ export const encodeFormDataFileName = ( fileList.forEach((file) => formData.append(key, file, encodeURI(file.name))); }; -export type FormDataFileName = 'attachments' | 'newAttachments' | 'pdf' | 'addFiles'; +export type FormDataFileName = 'attachments' | 'newAttachments' | 'pdf'; const isFile = (x: unknown): x is File => x instanceof File;