diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index df62c3275..165e604b0 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -4,6 +4,7 @@ on: push: branches: - main + - feat/search jobs: build: @@ -49,7 +50,7 @@ jobs: retry_wait_seconds: 1 retry_on: 'error' command: | - ssh -T -i ./deploy_key -p 22 ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} + ssh -T -i ./deploy_key -p 22 waffle@${{ secrets.SSH_HOST }} docker ps docker stop csereal_nextjs_image docker rm csereal_nextjs_image diff --git a/apis/index.ts b/apis/index.ts index 04bd3f4da..47983a7d3 100644 --- a/apis/index.ts +++ b/apis/index.ts @@ -10,6 +10,7 @@ export const getRequest = async ( ) => { const queryString = objToQueryString(params); const fetchUrl = `${BASE_URL}${url}${queryString}`; + console.log(fetchUrl); const response = await fetch(fetchUrl, { ...init, method: 'GET', @@ -30,6 +31,9 @@ export const postRequest = async (url: string, init?: RequestInit) return responseData as T; }; +export const postRequestWithCookie: typeof postRequest = (url, init) => + postRequest(url, { ...init, credentials: 'include' }); + export const patchRequest = async (url: string, init?: RequestInit) => { const fetchUrl = `${BASE_URL}${url}`; const response = await fetch(fetchUrl, { ...init, method: 'PATCH' }); @@ -40,15 +44,28 @@ export const patchRequest = async (url: string, init?: RequestInit) } }; +export const patchRequestWithCookie: typeof patchRequest = (url, init) => + patchRequest(url, { ...init, credentials: 'include' }); + export const deleteRequest = async (url: string, init?: RequestInit) => { const fetchUrl = `${BASE_URL}${url}`; const response = await fetch(fetchUrl, { ...init, method: 'DELETE' }); checkError(response); }; +export const deleteRequestWithCookie: typeof deleteRequest = async (url, init) => + deleteRequest(url, { ...init, credentials: 'include' }); + const checkError = (response: Response) => { if (!response.ok) { - throw new Error(`네트워크 에러 -status: ${response.status}`); + throw new NetworkError(response.status); } }; + +export class NetworkError extends Error { + statusCode: number; + constructor(statusCode: number) { + super(`네트워크 에러\nstatus: ${statusCode}`); + this.statusCode = statusCode; + } +} diff --git a/apis/notice.ts b/apis/notice.ts index 3f3a64c65..01a92f768 100644 --- a/apis/notice.ts +++ b/apis/notice.ts @@ -1,7 +1,7 @@ import { Notice, NoticePreviewList, POSTNoticeBody, PatchNoticeBody } from '@/types/notice'; import { PostSearchQueryParams } from '@/types/post'; -import { deleteRequest, getRequest, patchRequest, postRequest } from '.'; +import { deleteRequest, getRequest, patchRequest, postRequest, postRequestWithCookie } from '.'; const noticePath = '/notice'; @@ -23,7 +23,7 @@ export const postNotice = async (body: POSTNoticeBody) => { formData.append('attachments', attachment); } - await postRequest(noticePath, { + await postRequestWithCookie(noticePath, { body: formData, }); }; diff --git a/apis/reservation.ts b/apis/reservation.ts index fe4a7a76d..121abd40c 100644 --- a/apis/reservation.ts +++ b/apis/reservation.ts @@ -1,17 +1,22 @@ -// import { getMockWeeklyReservation, postMockReservation } from '@/data/reservation'; - import { cookies } from 'next/dist/client/components/headers'; -import { getMockWeeklyReservation } from '@/data/reservation'; - -import { Reservation, ReservationPostBody } from '@/types/reservation'; +import { Reservation, ReservationPostBody, ReservationPreview } from '@/types/reservation'; -import { deleteRequest, getRequest, postRequest } from '.'; +import { + deleteRequest, + deleteRequestWithCookie, + getRequest, + getRequestWithCookie, + postRequestWithCookie, +} from '.'; const reservationPath = '/reservation'; export const postReservation = async (body: ReservationPostBody) => { - await postRequest(reservationPath, { body: JSON.stringify(body) }); + await postRequestWithCookie(reservationPath, { + body: JSON.stringify(body), + headers: { 'Content-Type': 'application/json' }, + }); }; export const getWeeklyReservation = async (params: { @@ -26,15 +31,18 @@ export const getWeeklyReservation = async (params: { headers: { Cookie: `JSESSIONID=${jsessionId?.value}`, }, - })) as Reservation[]; + })) as ReservationPreview[]; }; +export const getReservation = async (id: number) => + getRequestWithCookie(`${reservationPath}/${id}`) as Promise; + export const deleteSingleReservation = async (id: number) => { - await deleteRequest(`${reservationPath}/${id}`); + await deleteRequestWithCookie(`${reservationPath}/${id}`); }; -export const deleteAllRecurringReservation = async (id: number) => { - await deleteRequest(`${reservationPath}/recurring/${id}`); +export const deleteAllRecurringReservation = async (id: string) => { + await deleteRequestWithCookie(`${reservationPath}/recurring/${id}`); }; export const roomNameToId = { diff --git a/apis/search.ts b/apis/search.ts new file mode 100644 index 000000000..1d05e5d06 --- /dev/null +++ b/apis/search.ts @@ -0,0 +1,86 @@ +import { getRequest } from '.'; + +export const getNoticeSearch = (params: { + keyword: string; + number: number; +}): Promise => getRequest('/notice/totalSearch', params); + +export const getNewsSearch = (params: { + keyword: string; + number: number; +}): Promise => getRequest('/news/totalSearch', params); + +// export const getNoticeSearch = async (params: { +// keyword: string; +// number: number; +// }): Promise => ({ +// total: 10, +// results: [ +// { +// id: 1, +// title: 'TITLE', +// createdAt: new Date().toISOString(), +// partialDescription: '12345678912345 789', +// boldStartIndex: 3, +// boldEndIndex: 7, +// }, +// { +// id: 2, +// title: 'TITLE2', +// createdAt: new Date().toISOString(), +// partialDescription: '12345678912345 789', +// boldStartIndex: 0, +// boldEndIndex: 4, +// }, +// { +// id: 3, +// title: 'TITLE3', +// createdAt: new Date().toISOString(), +// partialDescription: '12345678912345 789', +// boldStartIndex: 0, +// boldEndIndex: 4, +// }, +// ], +// }); + +// export const getNewsSearch = async (params: { +// keyword: string; +// number: number; +// }): Promise => ({ +// total: 3, +// results: [ +// { +// id: 1, +// title: 'TITLE1', +// date: new Date().toISOString(), +// partialDescription: +// "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.", +// boldStartIndex: 3, +// boldEndIndex: 7, +// tags: ['TAG1', 'TAG2'], +// imageUrl: null, +// }, +// { +// id: 2, +// title: 'TITLE2', +// date: new Date().toISOString(), +// partialDescription: +// "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.", +// boldStartIndex: 3, +// boldEndIndex: 7, +// tags: ['TAG1', 'TAG2'], +// imageUrl: null, +// }, +// { +// id: 3, +// title: 'TITLE3', +// date: new Date().toISOString(), +// partialDescription: +// "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.", +// boldStartIndex: 3, +// boldEndIndex: 7, +// tags: ['TAG1', 'TAG2'], +// imageUrl: null, +// }, +// ], +// }); diff --git a/apis/seminar.ts b/apis/seminar.ts index 2428e797e..eacfb7bbd 100644 --- a/apis/seminar.ts +++ b/apis/seminar.ts @@ -1,24 +1,24 @@ -import { getMockSeminarPost, getMockSeminarPosts } from '@/data/seminar'; +import { PostSearchQueryParams } from '@/types/post'; +import { PATCHSeminarBody, POSTSeminarBody, Seminar, SeminarList } from '@/types/seminar'; import { - GETSeminarPostsResponse, - SeminarPostResponse, - PostSearchQueryParams, - POSTSeminarBody, -} from '@/types/post'; - -import { getRequest, postRequest } from '.'; + deleteRequest, + getRequest, + patchRequestWithCookie, + postRequest, + postRequestWithCookie, +} from '.'; const seminarPath = '/seminar'; export const getSeminarPosts = async (params: PostSearchQueryParams) => { - return (await getRequest(seminarPath, params, { cache: 'no-store' })) as GETSeminarPostsResponse; + return (await getRequest(seminarPath, params, { cache: 'no-store' })) as SeminarList; }; export const getSeminarPost = async (id: number, params: PostSearchQueryParams) => { return (await getRequest(`${seminarPath}/${id}`, params, { cache: 'no-store', - })) as SeminarPostResponse; + })) as Seminar; }; export const postSeminar = async (body: POSTSeminarBody) => { @@ -32,11 +32,35 @@ export const postSeminar = async (body: POSTSeminarBody) => { ); if (body.image) { - formData.append('image', body.image); + formData.append('mainImage', body.image); } + for (const attachment of body.attachments) { formData.append('attachments', attachment); } - return (await postRequest(seminarPath, { body: formData })) as SeminarPostResponse; + await postRequestWithCookie(seminarPath, { body: formData }); }; + +export const editSeminar = async (id: number, body: PATCHSeminarBody) => { + const formData = new FormData(); + + formData.append( + 'request', + new Blob([JSON.stringify(body.request)], { + type: 'application/json', + }), + ); + + if (body.image) { + formData.append('mainImage', body.image); + } + + for (const attachment of body.newAttachments) { + formData.append('newAttachments', attachment); + } + + await patchRequestWithCookie(`${seminarPath}/${id}`, { body: formData }); +}; + +export const deleteSeminar = async (id: number) => deleteRequest(`${seminarPath}/${id}`); diff --git a/app/[locale]/academics/undergraduate/courses/page.tsx b/app/[locale]/academics/undergraduate/courses/page.tsx index f310bc730..2684b5fcf 100644 --- a/app/[locale]/academics/undergraduate/courses/page.tsx +++ b/app/[locale]/academics/undergraduate/courses/page.tsx @@ -90,8 +90,8 @@ const sortCourses = (courses: Course[], sortOption: SortOption) => { sortedCourses[getSortGroupIndexByClassification(course.classification)].push(course), ); } else { - sortedCourses.push([], [], []); - courses.forEach((course) => sortedCourses[course.credit - 2].push(course)); + sortedCourses.push([], [], [], []); + courses.forEach((course) => sortedCourses[course.credit - 1].push(course)); } return sortedCourses; diff --git a/app/[locale]/community/news/[id]/page.tsx b/app/[locale]/community/news/[id]/page.tsx index 19ab22830..e9560f1ec 100644 --- a/app/[locale]/community/news/[id]/page.tsx +++ b/app/[locale]/community/news/[id]/page.tsx @@ -33,7 +33,8 @@ export default async function NewsPostPage({ params, searchParams }: NewsPostPag diff --git a/app/[locale]/community/news/create/page.tsx b/app/[locale]/community/news/create/page.tsx index 33f4b5e3e..e6ef587f7 100644 --- a/app/[locale]/community/news/create/page.tsx +++ b/app/[locale]/community/news/create/page.tsx @@ -20,7 +20,6 @@ export default function NewsCreatePage() { const router = useRouter(); const handleComplete = async (content: PostEditorContent) => { - console.log(content.description); throwIfCantSubmit(content); const mainImage = @@ -30,8 +29,9 @@ export default function NewsCreatePage() { await postNews({ request: { title: content.title, + titleForMain: content.titleForMain ? content.titleForMain : null, description: content.description, - isPublic: content.isPublic, + isPrivate: content.isPrivate, isSlide: content.isSlide, isImportant: content.isImportant, tags: content.tags, diff --git a/app/[locale]/community/notice/[id]/page.tsx b/app/[locale]/community/notice/[id]/page.tsx index 7002fbeb2..08e6354bb 100644 --- a/app/[locale]/community/notice/[id]/page.tsx +++ b/app/[locale]/community/notice/[id]/page.tsx @@ -32,7 +32,7 @@ export default async function NoticePostPage({

{`'${rawId}'`}는 올바르지 않은 id입니다.

- + ); } @@ -53,7 +53,8 @@ export default async function NoticePostPage({ diff --git a/app/[locale]/community/notice/create/page.tsx b/app/[locale]/community/notice/create/page.tsx index e90cb4e6a..54ddda73e 100644 --- a/app/[locale]/community/notice/create/page.tsx +++ b/app/[locale]/community/notice/create/page.tsx @@ -28,8 +28,9 @@ export default function NoticeCreatePage() { await postNotice({ request: { title: content.title, + titleForMain: content.titleForMain ? content.titleForMain : null, description: content.description, - isPublic: content.isPublic, + isPrivate: content.isPrivate, isPinned: content.isPinned, isImportant: content.isImportant, tags: content.tags, diff --git a/app/[locale]/community/seminar/[id]/page.tsx b/app/[locale]/community/seminar/[id]/page.tsx index 7914e4ee9..6479b1ba0 100644 --- a/app/[locale]/community/seminar/[id]/page.tsx +++ b/app/[locale]/community/seminar/[id]/page.tsx @@ -1,4 +1,5 @@ import Link from 'next-intl/link'; +import { ReactNode } from 'react'; import { getSeminarPost } from '@/apis/seminar'; @@ -33,6 +34,7 @@ export default async function SeminarPostPage({ params, searchParams }: SeminarP
{currPost.attachments.length !== 0 && } +
- {currPost.speakerUrl ? ( - - 이름: {currPost.name} - - ) : ( -

이름: {currPost.name}

- )} + + {'이름: '} + {currPost.name} + {currPost.speakerTitle &&

직함: {currPost.speakerTitle}

} - {currPost.affiliationUrl ? ( - - 소속: {currPost.affiliation} - - ) : ( -

소속: {currPost.affiliation}

- )} + + {'소속: '} + {currPost.affiliation} +
주최: {currPost.host}
-
- 일시: {currPost.startDate} - {currPost.endDate} -
+ +
일시: {formatStartEndDate(currPost.startDate, currPost.endDate)}
+
장소: {currPost.location}
-
요약
- -
연사 소개
- + + {currPost.description && ( + <> +
요약
+ + + )} + + {currPost.introduction && ( + <> +
연사 소개
+ + + )}
+
); } + +const LinkOrText = ({ href, children }: { href: string | null; children: ReactNode }) => { + return href ? ( + + {children} + + ) : ( +

{children}

+ ); +}; + +const formatStartEndDate = (startDateStr: string, endDateStr: string | null) => { + const startDate = new Date(startDateStr); + if (endDateStr === null) { + if (startDate.getHours() === 0 && startDate.getMinutes() === 0) { + return new Date(startDateStr).toLocaleDateString('ko-KR'); + } else { + return new Date(startDateStr).toLocaleString('ko-KR'); + } + } else { + const endDate = new Date(endDateStr); + if (isSameDay(startDate, endDate)) { + return `${startDate.toLocaleDateString()} ${startDate.toLocaleTimeString('ko-KR', { + hour: '2-digit', + minute: '2-digit', + })} - ${endDate.toLocaleTimeString('ko-KR', { + hour: '2-digit', + minute: '2-digit', + })}`; + } else { + return `${startDate.toLocaleString('ko-KR')} - ${endDate.toLocaleString('ko-KR')}`; + } + } +}; + +const isSameDay = (lhs: Date, rhs: Date) => + lhs.getDate() === rhs.getDate() && + lhs.getMonth() === rhs.getMonth() && + lhs.getFullYear() === rhs.getFullYear(); diff --git a/app/[locale]/community/seminar/create/page.tsx b/app/[locale]/community/seminar/create/page.tsx index 554e90c69..4b8213626 100644 --- a/app/[locale]/community/seminar/create/page.tsx +++ b/app/[locale]/community/seminar/create/page.tsx @@ -4,7 +4,6 @@ import { useRouter } from 'next/navigation'; import { postSeminar } from '@/apis/seminar'; -import { infoToast } from '@/components/common/toast'; import { isLocalFile, isLocalImage } from '@/components/editor/PostEditorProps'; import SeminarEditor from '@/components/editor/SeminarEditor'; import { SeminarEditorContent } from '@/components/editor/SeminarEditorProps'; @@ -20,9 +19,7 @@ export default function SeminarCreatePage() { const router = useRouter(); const handleComplete = async (content: SeminarEditorContent) => { - if (content.title === '') { - throw new Error('제목을 입력해주세요'); - } + throwIfCantSubmit(content); const image = content.speaker.image && isLocalImage(content.speaker.image) @@ -33,6 +30,7 @@ export default function SeminarCreatePage() { await postSeminar({ request: { title: content.title, + titleForMain: content.titleForMain ? content.titleForMain : null, description: content.description, introduction: content.speaker.description, name: content.speaker.name, @@ -40,11 +38,11 @@ export default function SeminarCreatePage() { speakerTitle: content.speaker.title, affiliation: content.speaker.organization, affiliationURL: content.speaker.organizationURL, - startDate: content.schedule.startDate.toLocaleDateString(), - endDate: content.schedule.endDate?.toLocaleDateString() ?? null, + startDate: content.schedule.startDate.toISOString(), + endDate: content.schedule.endDate?.toISOString() ?? null, location: content.location, host: content.host, - isPublic: content.isPublic, + isPrivate: content.isPrivate, isImportant: content.isImportant, }, image, @@ -65,3 +63,9 @@ export default function SeminarCreatePage() { ); } + +const throwIfCantSubmit = (content: SeminarEditorContent) => { + if (content.title === '') { + throw new Error('제목을 입력해주세요'); + } +}; diff --git a/app/[locale]/people/faculty/[id]/page.tsx b/app/[locale]/people/faculty/[id]/page.tsx index cbfc4359c..2b51001ef 100644 --- a/app/[locale]/people/faculty/[id]/page.tsx +++ b/app/[locale]/people/faculty/[id]/page.tsx @@ -43,7 +43,7 @@ export default async function FacultyMemberPage({ params }: { params: { id: numb
{data?.labName} diff --git a/app/[locale]/reservations/[roomType]/[roomName]/page.tsx b/app/[locale]/reservations/[roomType]/[roomName]/page.tsx index 5c665c718..de432f4e7 100644 --- a/app/[locale]/reservations/[roomType]/[roomName]/page.tsx +++ b/app/[locale]/reservations/[roomType]/[roomName]/page.tsx @@ -19,7 +19,7 @@ export default function RoomReservationPage({ params, searchParams }: RoomReserv ); } -export async function LoginedRoomReservationPage({ params, searchParams }: RoomReservationProps) { +async function LoginedRoomReservationPage({ params, searchParams }: RoomReservationProps) { const roomId = isValidRoomName(params.roomName) ? roomNameToId[params.roomName] : undefined; const date = parseDate(searchParams.selectedDate || todayYMDStr()); @@ -49,11 +49,12 @@ export async function LoginedRoomReservationPage({ params, searchParams }: RoomR }); return ( - + ); } diff --git a/app/[locale]/search/page.tsx b/app/[locale]/search/page.tsx new file mode 100644 index 000000000..6283ff1ae --- /dev/null +++ b/app/[locale]/search/page.tsx @@ -0,0 +1,201 @@ +export const dynamic = 'force-dynamic'; + +import { useTranslations } from 'next-intl'; +import Link from 'next-intl/link'; + +import { getNewsSearch, getNoticeSearch } from '@/apis/search'; +import { getSeminarPosts } from '@/apis/seminar'; + +import { CurvedHorizontalNode, StraightNode } from '@/components/common/Nodes'; +import NewsRow from '@/components/news/NewsRow'; +import NoticeRow from '@/components/search/NoticeRow'; +import SearchForm from '@/components/search/SearchForm'; +import SearchSubNav, { SearchSubNavProps } from '@/components/search/SearchSubNav'; +import SeminarRow from '@/components/seminar/SeminarRow'; + +import { news, notice, seminar } from '@/types/page'; + +import { getPath } from '@/utils/page'; + +const newsPath = getPath(news); +const noticePath = getPath(notice); +const seminarPath = getPath(seminar); + +interface SearchPageProps { + searchParams: { + query: string; + }; +} + +export default async function SearchPage({ searchParams: { query } }: SearchPageProps) { + const noticeSearchResult = await getNoticeSearch({ keyword: query, number: 2 }); + const newsSearchResult = await getNewsSearch({ keyword: query, number: 2 }); + const seminarSearchResult = await getSeminarPosts({ keyword: query, pageNum: 1 }); + + const total = noticeSearchResult.total + newsSearchResult.total + seminarSearchResult.total; + const searchNavProps: SearchSubNavProps = { + total, + nodes: [ + { + type: 'INTERNAL', + title: '소식', + size: total, + children: [ + { + type: 'LEAF', + title: '공지사항', + size: noticeSearchResult.total, + href: `${noticePath}?keyword=${query}`, + }, + { + type: 'LEAF', + title: '새 소식', + size: newsSearchResult.total, + href: `${newsPath}?keyword=${query}`, + }, + { + type: 'LEAF', + title: '세미나', + size: seminarSearchResult.total, + href: `${seminarPath}?keyword=${query}`, + }, + ], + }, + ], + }; + + return ( +
+ + + {/* 검색 결과 */} +
+ + + {/* 공지사항 */} + +
+ {noticeSearchResult.results.slice(0, 2).map((notice) => ( + + ))} +
+ + + + {/* 새소식 */} + +
+ {newsSearchResult.results.slice(0, 2).map((news) => ( + + ))} +
+ + + + {/* 세미나 */} + +
+ {seminarSearchResult.searchList.slice(0, 2).map((seminar) => ( + + ))} +
+ + +
+ + +
+ ); +} + +const PageHeader = ({ query }: { query: string }) => { + const t = useTranslations('Nav'); + + return ( +
+
+ +

+ {t('통합 검색')} +

+
+ + +
+ ); +}; + +const SectionTitle = ({ title, size }: { title: string; size: number }) => { + const t = useTranslations('Nav'); + return ( +
+

+ {t(title)}({size}) +

+
+
+
+
+
+ ); +}; + +const SectionSubtitle = ({ title, size }: { title: string; size: number }) => { + const t = useTranslations('Nav'); + return ( +
+
+

+ {t(title)}({size}) +

+
+ ); +}; + +const MoreResultLink = ({ href }: { href: string }) => { + const t = useTranslations('Nav'); + return ( + + {t('결과 더보기')} + chevron_right + + ); +}; + +const Divider = () =>
; diff --git a/components/common/AdjPostNav.tsx b/components/common/AdjPostNav.tsx index 69f4f8ac2..29d4922b8 100644 --- a/components/common/AdjPostNav.tsx +++ b/components/common/AdjPostNav.tsx @@ -2,19 +2,41 @@ import Link from 'next-intl/link'; import { AdjPostInfo } from '@/types/post'; +import LoginStaffVisible from './LoginStaffVisible'; +import PostDeleteButton from './PostDeleteButton'; + +type PostType = 'notice' | 'seminar' | 'news'; + interface AdjPostNavProps { + postType: PostType; + id?: string; prevPost?: AdjPostInfo; nextPost?: AdjPostInfo; - href: string; margin?: string; } -export default function AdjPostNav({ prevPost, nextPost, margin = '', href }: AdjPostNavProps) { +export default function AdjPostNav({ + prevPost, + nextPost, + margin = '', + postType, + id, +}: AdjPostNavProps) { return (
- +
+ + + {id && ( + <> + + + + )} + +
); } @@ -68,9 +90,20 @@ function PostListLink({ href }: { href: string }) { return ( 목록 ); } + +function PostEditLink({ href }: { href: string }) { + return ( + + 편집 + + ); +} diff --git a/components/common/Nodes.tsx b/components/common/Nodes.tsx index 7d7af09e4..c91a86cf2 100644 --- a/components/common/Nodes.tsx +++ b/components/common/Nodes.tsx @@ -1,5 +1,3 @@ -import { CSSProperties } from 'react'; - interface CurvedNodeProps { grow?: boolean; // flex-grow 속성 (true일 때는 부모 element가 'display: flex'여야 함) direction?: 'row' | 'col'; // 가로 노드: row, 세로 노드: col diff --git a/components/common/PostDeleteButton.tsx b/components/common/PostDeleteButton.tsx new file mode 100644 index 000000000..ea82619bb --- /dev/null +++ b/components/common/PostDeleteButton.tsx @@ -0,0 +1,20 @@ +'use client'; + +import { useRouter } from 'next/navigation'; + +import { deleteRequestWithCookie } from '@/apis'; + +export default function PostDeleteButton({ postType, id }: { postType: string; id: string }) { + const router = useRouter(); + return ( + + ); +} diff --git a/components/editor/PostEditor.tsx b/components/editor/PostEditor.tsx index c0771af10..6d5b6cdfc 100644 --- a/components/editor/PostEditor.tsx +++ b/components/editor/PostEditor.tsx @@ -54,6 +54,11 @@ export default function PostEditor({
+ + {showMainImage && ( @@ -84,10 +89,10 @@ export default function PostEditor({
{ - setContentByKey('isPublic')(!content.isPublic); - if (content.isPublic) { + setContentByKey('isPrivate')(!content.isPrivate); + if (content.isPrivate) { setContentByKey('isPinned')(false); } }} @@ -99,7 +104,7 @@ export default function PostEditor({ toggleCheck={() => { setContentByKey('isPinned')(!content.isPinned); if (!content.isPinned) { - setContentByKey('isPublic')(true); + setContentByKey('isPrivate')(true); } }} /> @@ -140,7 +145,7 @@ export default function PostEditor({ function TitleFieldset({ value, onChange }: { value: string; onChange: (text: string) => void }) { return ( -
+
void; +}) { + return ( +
+ +
+ ); +} + function EditorFieldset({ editorRef, initialContent, @@ -159,7 +183,7 @@ function EditorFieldset({ initialContent: string; }) { return ( -
+
); diff --git a/components/editor/PostEditorProps.ts b/components/editor/PostEditorProps.ts index f7f28173a..feb6dea99 100644 --- a/components/editor/PostEditorProps.ts +++ b/components/editor/PostEditorProps.ts @@ -10,6 +10,7 @@ export interface LocalFile { export interface UploadedFile { type: 'UPLOADED_FILE'; file: { + id: number; name: string; url: string; bytes: number; @@ -38,11 +39,12 @@ export const isLocalImage = (image: LocalImage | UploadedImage): image is LocalI export interface PostEditorContent { title: string; + titleForMain: string; description: string; mainImage: PostEditorImage; attachments: PostEditorFile[]; tags: string[]; - isPublic: boolean; + isPrivate: boolean; isPinned: boolean; isImportant: boolean; isSlide: boolean; @@ -60,11 +62,12 @@ export interface PostEditorProps { export const postEditorDefaultValue: PostEditorContent = { title: '', + titleForMain: '', description: '', mainImage: null, attachments: [], tags: [], - isPublic: true, + isPrivate: true, isPinned: false, isImportant: false, isSlide: false, diff --git a/components/editor/SeminarEditor.tsx b/components/editor/SeminarEditor.tsx index 5b5ea9880..b5869f543 100644 --- a/components/editor/SeminarEditor.tsx +++ b/components/editor/SeminarEditor.tsx @@ -60,6 +60,10 @@ export default function SeminarEditor({ actions, initialContent }: SeminarEditor return ( + @@ -111,6 +115,25 @@ function TitleFieldset({ value, onChange }: { value: string; onChange: (text: st ); } +function TitleForMainFieldset({ + value, + onChange, +}: { + value: string; + onChange: (text: string) => void; +}) { + return ( +
+ +
+ ); +} + function SummaryEditorFieldset({ summaryEditorRef, initialContent, @@ -181,7 +204,7 @@ function ScheduleFieldset({ isChecked={values.endDate !== null} toggleCheck={(tag, isChecked) => { if (isChecked) { - setValues('endDate')(new Date()); + setValues('endDate')(values.startDate); } else { setValues('endDate')(null); } @@ -310,14 +333,14 @@ function FileFieldset({ files, setFiles }: FilePickerProps) { } function CheckboxFieldset({ - isPublic, - setIsPublic, + isPrivate, + setIsPrivate, isImportant, setIsImportant, }: { - isPublic: boolean; + isPrivate: boolean; isImportant: boolean; - setIsPublic: (value: boolean) => void; + setIsPrivate: (value: boolean) => void; setIsImportant: (value: boolean) => void; }) { return ( @@ -325,8 +348,8 @@ function CheckboxFieldset({
setIsPublic(!isChecked)} + isChecked={isPrivate} + toggleCheck={(tag, isChecked) => setIsPrivate(isChecked)} /> { return { title: '', + titleForMain: '', description: '', location: '', schedule: { @@ -60,7 +62,7 @@ export const getSeminarEditorDefaultValue = (): SeminarEditorContent => { image: null, }, attachments: [], - isPublic: true, + isPrivate: false, isImportant: false, }; }; diff --git a/components/editor/common/FilePicker.tsx b/components/editor/common/FilePicker.tsx index 5ec8c91e6..ad6620013 100644 --- a/components/editor/common/FilePicker.tsx +++ b/components/editor/common/FilePicker.tsx @@ -41,7 +41,10 @@ export default function FilePicker({ files, setFiles }: FilePickerProps) { // 순서를 안바꾸기로 했으니 키값으로 인덱스 써도 ㄱㅊ key={idx} file={item} - deleteFile={() => deleteFileAtIndex(idx)} + deleteFile={(e) => { + e.preventDefault(); + deleteFileAtIndex(idx); + }} /> ))} diff --git a/components/editor/common/suneditor.custom.css b/components/editor/common/suneditor.custom.css index 146ca980b..f4614d5fb 100644 --- a/components/editor/common/suneditor.custom.css +++ b/components/editor/common/suneditor.custom.css @@ -2310,6 +2310,8 @@ textarea.se-error:focus { } .sun-editor-editable table td, .sun-editor-editable table th { + /* 가끔 html에 스타일이 수동으로 들어간 경우가 있어서 임시 조치 */ + background-color: transparent !important; border: 1px solid #e1e1e1; padding: 0.4em; background-clip: padding-box; diff --git a/components/facilities/FacilitiesRow.tsx b/components/facilities/FacilitiesRow.tsx index ed0c717a8..e8ab4fca7 100644 --- a/components/facilities/FacilitiesRow.tsx +++ b/components/facilities/FacilitiesRow.tsx @@ -1,4 +1,7 @@ +'use client'; + import Image from 'next/image'; +import { useState } from 'react'; import HTMLViewer from '../editor/HTMLViewer'; @@ -15,8 +18,13 @@ export default function FacilitiesRow({ location, imageURL, }: FacilitiesRowProps) { + const [hover, setHover] = useState(false); return ( -
+
setHover(true)} + onMouseLeave={() => setHover(false)} + >

{name} @@ -30,15 +38,19 @@ export default function FacilitiesRow({

- + ); } -function FacilitiesRowImage({ imageURL }: { imageURL: string }) { +function FacilitiesRowImage({ imageURL, hover }: { imageURL: string; hover: boolean }) { + const src = hover + ? `/image/facilities/${imageURL}-color.png` + : `/image/facilities/${imageURL}-original.png`; + return (
- 대표 이미지 + 대표 이미지
); } diff --git a/components/layout/header/HeaderSearchBar.tsx b/components/layout/header/HeaderSearchBar.tsx index 7e0a6b687..ee39b7212 100644 --- a/components/layout/header/HeaderSearchBar.tsx +++ b/components/layout/header/HeaderSearchBar.tsx @@ -1,22 +1,22 @@ 'use client'; -import { useState, ChangeEventHandler, KeyboardEventHandler } from 'react'; - -import { useCustomSearchParams } from '@/hooks/useCustomSearchParams'; +import { useRouter } from 'next/navigation'; +import { useState, ChangeEventHandler, FormEventHandler } from 'react'; export default function HeaderSearchBar() { const [text, setText] = useState(''); - - // TODO: /search 페이지 구현 - const { setSearchParams } = useCustomSearchParams('/search'); + const router = useRouter(); const handleChange: ChangeEventHandler = (e) => { setText(e.target.value); }; - const searchText = () => { - const trimmedText = text.trim(); - if (trimmedText) setSearchParams({ keyword: trimmedText, purpose: 'search' }); + const searchText: FormEventHandler = (e) => { + e.preventDefault(); + const query = text.trim(); + if (query) { + router.push(`/search?query=${query}`); + } }; return ( diff --git a/components/news/AdminFeatures.tsx b/components/news/AdminFeatures.tsx new file mode 100644 index 000000000..19da4a94a --- /dev/null +++ b/components/news/AdminFeatures.tsx @@ -0,0 +1,20 @@ +import Link from 'next/link'; + +import { news } from '@/types/page'; + +import { getPath } from '@/utils/page'; + +const newsPath = getPath(news); + +export default function AdminFeatures() { + return ( +
+ + 새 게시글 + +
+ ); +} diff --git a/components/news/EditNewsPageContent.tsx b/components/news/EditNewsPageContent.tsx index bac51c91f..70b6a2b65 100644 --- a/components/news/EditNewsPageContent.tsx +++ b/components/news/EditNewsPageContent.tsx @@ -31,7 +31,7 @@ export default function EditNewsPageContent({ id, data }: { id: number; data: Ne title: data.title, description: data.description, - isPublic: data.isPublic, + isPrivate: data.isPrivate, attachments: data.attachments.map((file) => ({ type: 'UPLOADED_FILE', file })), tags: data.tags, @@ -49,15 +49,20 @@ export default function EditNewsPageContent({ id, data }: { id: number; data: Ne const mainImage = content.mainImage && isLocalImage(content.mainImage) ? content.mainImage.file : null; + const deleteIds = data.attachments + .map((x) => x.id) + .filter((id1) => uploadedAttachments.find((x) => x.id === id1) === undefined); + await patchNews(id, { request: { title: content.title, + titleForMain: content.titleForMain ? content.titleForMain : null, description: content.description, - isPublic: content.isPublic, + isPrivate: content.isPrivate, isSlide: content.isSlide, isImportant: content.isImportant, tags: content.tags, - attachments: uploadedAttachments, + deleteIds, }, mainImage, newAttachments: localAttachments, diff --git a/components/news/NewsPageContent.tsx b/components/news/NewsPageContent.tsx index 9eb6d8c67..819c468b7 100644 --- a/components/news/NewsPageContent.tsx +++ b/components/news/NewsPageContent.tsx @@ -1,5 +1,7 @@ 'use client'; +import Link from 'next/link'; + import Pagination from '@/components/common/Pagination'; import SearchBox from '@/components/common/search/SearchBox'; import PageLayout from '@/components/layout/pageLayout/PageLayout'; @@ -14,6 +16,9 @@ import { news } from '@/types/page'; import { getPath } from '@/utils/page'; +import AdminFeatures from './AdminFeatures'; +import LoginStaffVisible from '../common/LoginStaffVisible'; + const POST_LIMIT = 10; const newsPath = getPath(news); @@ -38,6 +43,9 @@ export default function NewsPageContent({ setSearchParams={setSearchParams} />
+ {searchList.length === 0 && ( +

검색 결과가 존재하지 않습니다.

+ )} {searchList.map((post) => ( + + + ); } diff --git a/components/news/NewsRow.tsx b/components/news/NewsRow.tsx index 18faf16a3..ecd203dda 100644 --- a/components/news/NewsRow.tsx +++ b/components/news/NewsRow.tsx @@ -1,8 +1,5 @@ -import Image from 'next/image'; import Link from 'next-intl/link'; -import SNULogo from '@/public/image/SNU_Logo.svg'; // 추후 수정 - import Tags from '@/components/common/Tags'; import { news } from '@/types/page'; @@ -18,11 +15,26 @@ export interface NewsRowProps { tags: string[]; date: Date; imageURL: string | null; + + descriptionBold?: { + startIndex: number; + endIndex: number; + }; + hideDivider?: boolean; } const newsPath = getPath(news); -export default function NewsRow({ href, title, description, tags, date, imageURL }: NewsRowProps) { +export default function NewsRow({ + href, + title, + description, + tags, + date, + imageURL, + descriptionBold, + hideDivider, +}: NewsRowProps) { description += '...'; // clip이 안될정도로 화면이 좌우로 긴 경우 대비 const dateStr = date.toLocaleDateString('ko', { @@ -33,19 +45,39 @@ export default function NewsRow({ href, title, description, tags, date, imageURL }); return ( -
+
- +

{title}

+ -

{description}

+

+ {descriptionBold ? ( + <> + {description.slice(0, descriptionBold.startIndex)} + + {description.slice(descriptionBold.startIndex, descriptionBold.endIndex)} + + {description.slice(descriptionBold.endIndex)} + + ) : ( + description + )} +

+
- + +
+ ({ type: 'UPLOADED_FILE', file })), tags: data.tags, @@ -43,15 +43,20 @@ export default function EditNoticePageContent({ id, data }: { id: number; data: const uploadedAttachments = content.attachments.filter(isUploadedFile).map((x) => x.file); const localAttachments = content.attachments.filter(isLocalFile).map((x) => x.file); + const deleteIds = data.attachments + .map((x) => x.id) + .filter((id1) => uploadedAttachments.find((x) => x.id === id1) === undefined); + await patchNotice(id, { request: { title: content.title, + titleForMain: content.titleForMain ? content.titleForMain : null, description: content.description, - isPublic: content.isPublic, + isPrivate: content.isPrivate, isPinned: content.isPinned, isImportant: content.isImportant, tags: content.tags, - attachments: uploadedAttachments, + deleteIds, }, newAttachments: localAttachments, }); diff --git a/components/notice/NoticeListRow.tsx b/components/notice/NoticeListRow.tsx index d34236363..f1773d808 100644 --- a/components/notice/NoticeListRow.tsx +++ b/components/notice/NoticeListRow.tsx @@ -35,7 +35,7 @@ export default function NoticeListRow({ return (
  • {isEditMode && ( @@ -44,7 +44,7 @@ export default function NoticeListRow({ toggleCheck={() => toggleSelected(post.id, isSelected)} /> )} - + - changeSelectedIds({ type: 'RESET' })} - /> + + changeSelectedIds({ type: 'RESET' })} + /> + ); } diff --git a/components/people/FacultyInfoWithImage.tsx b/components/people/FacultyInfoWithImage.tsx index a1580e012..2779cde2a 100644 --- a/components/people/FacultyInfoWithImage.tsx +++ b/components/people/FacultyInfoWithImage.tsx @@ -1,5 +1,4 @@ import Link from 'next-intl/link'; -import { useEffect, useState } from 'react'; import PeopleImageWithAnimation from './PeopleImageWithAnimation'; @@ -43,7 +42,7 @@ export default function FacultyInfoWithImage({ className="h-full w-full absolute bottom-[-17px] right-[-17px] animate-fadeIn" style={{ background: - 'repeating-linear-gradient(-45deg, white, white 5px, orange 5px, orange 6px)', + 'repeating-linear-gradient(-45deg, black, black 5px, #E9390B 5px, #E9390B 6px)', }} />
  • diff --git a/components/people/PeopleImageWithAnimation.tsx b/components/people/PeopleImageWithAnimation.tsx index 3775c3cb2..03d8a229f 100644 --- a/components/people/PeopleImageWithAnimation.tsx +++ b/components/people/PeopleImageWithAnimation.tsx @@ -30,7 +30,7 @@ export default function PeopleImageWithAnimation({ imageURL }: PeopleImageWithAn className="h-full w-full absolute bottom-[-17px] left-[-17px] animate-fadeIn" style={{ background: - 'repeating-linear-gradient(-45deg, white, white 5px, orange 5px, orange 6px)', + 'repeating-linear-gradient(-45deg, black, black 5px, #E9390B 5px, #E9390B 6px)', clipPath: 'polygon(84.375% 0%, 100% 11.71875%, 100% 100%, 0% 100%, 0% 0%)', }} /> diff --git a/components/reservations/ReservationCalendar.tsx b/components/reservations/ReservationCalendar.tsx index 549abb069..6e09e83c1 100644 --- a/components/reservations/ReservationCalendar.tsx +++ b/components/reservations/ReservationCalendar.tsx @@ -1,4 +1,4 @@ -import { Reservation } from '@/types/reservation'; +import { Reservation, ReservationPreview } from '@/types/reservation'; import CalendarContent from './calendar/CalendarContent'; import Toolbar from './calendar/CalendarToolbar'; @@ -7,19 +7,22 @@ export default function ReservationCalendar({ startDate, selectedDate, reservations, + roomId, }: { startDate: Date; selectedDate: Date; - reservations: Reservation[]; + reservations: ReservationPreview[]; + roomId: number; }) { const headerText = `${selectedDate.getFullYear()}/${(selectedDate.getMonth() + 1 + '').padStart( 2, '0', )}`; + return (

    {headerText}

    - +
    ); diff --git a/components/reservations/calendar/CalendarColumn.tsx b/components/reservations/calendar/CalendarColumn.tsx index 9825d7876..4c19fc961 100644 --- a/components/reservations/calendar/CalendarColumn.tsx +++ b/components/reservations/calendar/CalendarColumn.tsx @@ -1,4 +1,4 @@ -import { Reservation } from '@/types/reservation'; +import { Reservation, ReservationPreview } from '@/types/reservation'; import styles from './cellstyle.module.css'; import { ReservationDetailModalButton } from '../modals/ReservationDetailModal'; @@ -10,7 +10,7 @@ export default function CalendarColumn({ }: { date: Date; selected: boolean; - reservations: Reservation[]; + reservations: ReservationPreview[]; }) { return (
    @@ -52,16 +52,18 @@ const ColumnBackground = ({ selected }: { selected: boolean }) => { )); }; -const CalendarCell = ({ reservation }: { reservation: Reservation }) => { +const CalendarCell = ({ reservation }: { reservation: ReservationPreview }) => { // 셀 높이 구하기 // 30분으로 나눴을 때 몇 칸인지 - const unitCnt = - (reservation.endTime.getTime() - reservation.startTime.getTime()) / 1000 / 60 / 30; + + const startTime = new Date(reservation.startTime); + const endTime = new Date(reservation.endTime); + + const unitCnt = (endTime.getTime() - startTime.getTime()) / 1000 / 60 / 30; const unitHeightInREM = 1.5; const height = unitCnt * unitHeightInREM; // 셀 y축 offset 구하기 - const { startTime, endTime } = reservation; const topTime = new Date( startTime.getFullYear(), startTime.getMonth(), @@ -76,11 +78,13 @@ const CalendarCell = ({ reservation }: { reservation: Reservation }) => { endTime.getHours(), )}:${padZero(endTime.getMinutes())}`; + console.log(reservation.id); + return ( {unitCnt !== 1 && (
    @@ -88,7 +92,7 @@ const CalendarCell = ({ reservation }: { reservation: Reservation }) => {
    )}
    -

    {reservation.userName}

    +

    {reservation.title}

    ); diff --git a/components/reservations/calendar/CalendarContent.tsx b/components/reservations/calendar/CalendarContent.tsx index 7debcfb24..5c8d962cd 100644 --- a/components/reservations/calendar/CalendarContent.tsx +++ b/components/reservations/calendar/CalendarContent.tsx @@ -1,4 +1,4 @@ -import { Reservation } from '@/types/reservation'; +import { ReservationPreview } from '@/types/reservation'; import CalendarColumn from './CalendarColumn'; @@ -7,7 +7,7 @@ export default function CalendarContent({ reservations, }: { startDate: Date; - reservations: Reservation[]; + reservations: ReservationPreview[]; }) { const dates = getNextSevenDays(startDate); const today = new Date(); @@ -53,13 +53,19 @@ const getNextSevenDays = (date: Date) => { }); }; -const isSameDay = (date1: Date, date2: Date) => - date1.getFullYear() === date2.getFullYear() && - date1.getMonth() === date2.getMonth() && - date1.getDate() === date2.getDate(); +const isSameDay = (date1: Date, date2: Date) => { + return ( + date1.getFullYear() === date2.getFullYear() && + date1.getMonth() === date2.getMonth() && + date1.getDate() === date2.getDate() + ); +}; -const isReservationInDate = (reservation: Reservation, date: Date): boolean => { - return isSameDay(reservation.startTime, date) && isSameDay(reservation.endTime, date); +const isReservationInDate = (reservation: ReservationPreview, date: Date): boolean => { + return ( + isSameDay(new Date(reservation.startTime), date) && + isSameDay(new Date(reservation.endTime), date) + ); }; const rows = [ diff --git a/components/reservations/calendar/CalendarToolbar.tsx b/components/reservations/calendar/CalendarToolbar.tsx index 50d4a6100..a359fa9f4 100644 --- a/components/reservations/calendar/CalendarToolbar.tsx +++ b/components/reservations/calendar/CalendarToolbar.tsx @@ -6,7 +6,7 @@ import { TodayButton, } from './NavigateButtons'; -export default function Toolbar({ date }: { date: Date }) { +export default function Toolbar({ date, roomId }: { date: Date; roomId: number }) { const today = new Date(); const todayButtonHidden = today.getFullYear() === date.getFullYear() && @@ -23,7 +23,7 @@ export default function Toolbar({ date }: { date: Date }) {
    - +
    ); diff --git a/components/reservations/calendar/NavigateButtons.tsx b/components/reservations/calendar/NavigateButtons.tsx index d7cacbbae..d6113eba7 100644 --- a/components/reservations/calendar/NavigateButtons.tsx +++ b/components/reservations/calendar/NavigateButtons.tsx @@ -103,10 +103,10 @@ export function NextWeekButton({ date }: { date: Date }) { ); } -export function MakeReservationButton() { +export function MakeReservationButton({ roomId }: { roomId: number }) { const { openModal } = useModal(); const handleClick = () => { - openModal(); + openModal(); }; return ( diff --git a/components/reservations/modals/AddReservationModal.tsx b/components/reservations/modals/AddReservationModal.tsx index 69ae057ff..d3cccef60 100644 --- a/components/reservations/modals/AddReservationModal.tsx +++ b/components/reservations/modals/AddReservationModal.tsx @@ -3,7 +3,12 @@ import Link from 'next-intl/link'; import { FormEventHandler, ReactNode, useReducer, useState } from 'react'; +import { NetworkError } from '@/apis'; + +import { postReservation } from '@/apis/reservation'; + import Dropdown from '@/components/common/Dropdown'; +import { errorToast, infoToast } from '@/components/common/toast'; import ModalFrame from '@/components/modal/ModalFrame'; import MuiDateSelector from '@/components/mui/MuiDateSelector'; import BasicButton from '@/components/reservations/BasicButton'; @@ -12,18 +17,37 @@ import useModal from '@/hooks/useModal'; import { ReservationPostBody } from '@/types/reservation'; -export default function AddReservationModal() { +export default function AddReservationModal({ roomId }: { roomId: number }) { const { closeModal } = useModal(); const [privacyChecked, togglePrivacyChecked] = useReducer((x) => !x, false); - const [body, setBody] = useState(getDefaultBodyValue); + const [body, setBody] = useState(getDefaultBodyValue(roomId)); - const canSubmit = - privacyChecked && body.title !== '' && body.contactEmail !== '' && body.professor !== ''; - - const handleSubmit: FormEventHandler = (e) => { + const handleSubmit: FormEventHandler = async (e) => { e.preventDefault(); - if (!canSubmit) return; - console.log(body); + + const canSubmit = + privacyChecked && body.title !== '' && body.contactEmail !== '' && body.professor !== ''; + + if (!canSubmit) { + infoToast('모든 필수 정보를 입력해주세요'); + return; + } + try { + await postReservation(body); + window.location.reload(); + } catch (e) { + if (e instanceof NetworkError) { + if (e.statusCode === 409) { + errorToast('해당 위치에 예약이 존재합니다.'); + } else { + errorToast(e.message); + } + } else if (e instanceof Error) { + errorToast(e.message); + } else { + errorToast('알 수 없는 에러'); + } + } }; const buildBodyValueSetter = @@ -43,12 +67,16 @@ export default function AddReservationModal() { newStartTime = convertToFastestStartTime(newStartTime); } - const endTime = getOptimalEndTime(body.startTime, body.endTime, newStartTime); + const endTime = getOptimalEndTime( + new Date(body.startTime), + new Date(body.endTime), + newStartTime, + ); const startTimeSetter = buildBodyValueSetter('startTime'); const endTimeSetter = buildBodyValueSetter('endTime'); - startTimeSetter(newStartTime); - endTimeSetter(endTime); + startTimeSetter(newStartTime.toISOString()); + endTimeSetter(endTime.toISOString()); }; return ( @@ -61,23 +89,23 @@ export default function AddReservationModal() {
    - +
    buildBodyValueSetter('startTime')(x.toISOString())} + setEndDate={(x) => buildBodyValueSetter('endTime')(x.toISOString())} /> buildBodyValueSetter('endTime')(x.toISOString())} />
    @@ -87,8 +115,8 @@ export default function AddReservationModal() { contents={Array(14) .fill(0) .map((_, i) => i + 1 + '회')} - selectedIndex={body.recurringWeeks} - onClick={buildBodyValueSetter('recurringWeeks')} + selectedIndex={body.recurringWeeks - 1} + onClick={(x) => buildBodyValueSetter('recurringWeeks')(x + 1)} />
    @@ -158,7 +186,8 @@ const DateInput = ({ date, setDate }: { date: Date; setDate: (date: Date) => voi
    - +
    { +const DeleteButtons = ({ + reservationId, + recurrenceId, +}: { + reservationId: number; + recurrenceId: string; +}) => { const [submitting, setSubmitting] = useState(false); const { closeModal } = useModal(); @@ -68,10 +83,10 @@ const DeleteButtons = ({ reservationId }: { reservationId: number }) => { if (submitting) return; setSubmitting(true); try { - await deleteAllRecurringReservation(reservationId); - closeModal(); - } catch { - errorToast('문제가 발생했습니다'); + await deleteAllRecurringReservation(recurrenceId); + window.location.reload(); + } catch (e) { + toastError(e); setSubmitting(false); } }; @@ -81,9 +96,9 @@ const DeleteButtons = ({ reservationId }: { reservationId: number }) => { setSubmitting(true); try { await deleteSingleReservation(reservationId); - closeModal(); - } catch { - errorToast('문제가 발생했습니다'); + window.location.reload(); + } catch (e) { + toastError(e); setSubmitting(false); } }; @@ -101,19 +116,27 @@ const DeleteButtons = ({ reservationId }: { reservationId: number }) => { }; export const ReservationDetailModalButton = ({ - reservation, + reservationId, ...props }: DetailedHTMLProps, HTMLButtonElement> & { - reservation: Reservation; + reservationId: number; }) => { const { openModal } = useModal(); return ( +
    + + ); +} diff --git a/components/search/SearchSubNav.tsx b/components/search/SearchSubNav.tsx new file mode 100644 index 000000000..49cbae63c --- /dev/null +++ b/components/search/SearchSubNav.tsx @@ -0,0 +1,70 @@ +import { useTranslations } from 'next-intl'; +import Link from 'next-intl/link'; + +import { CurvedVerticalNode } from '../common/Nodes'; + +export interface SearchSubNavProps { + total: number; + nodes: SearchSubNavNodeProps[]; +} + +export default function SearchSubNav({ total, nodes }: SearchSubNavProps) { + const t = useTranslations('Nav'); + + return ( +
    + +
    +

    {t('통합 검색')}

    +
    +

    + {t('전체')}({total}) +

    + {nodes.map((node, idx) => ( + + ))} +
    +
    +
    + ); +} + +type SearchSubNavNodeProps = { + title: string; + size: number; +} & ( + | { + type: 'LEAF'; + href: string; + } + | { + type: 'INTERNAL'; + children: SearchSubNavNodeProps[]; + } +); + +const SearchSubNavNode = (props: SearchSubNavNodeProps) => { + const t = useTranslations('Nav'); + const { title, size, type } = props; + + if (type === 'INTERNAL') { + return ( +
    +

    + {t(title)}({size}) +

    +
    + {props.children.map((node, idx) => ( + + ))} +
    +
    + ); + } else { + return ( + + {t(title)}({size}) + + ); + } +}; diff --git a/components/seminar/AdminFeatures.tsx b/components/seminar/AdminFeatures.tsx new file mode 100644 index 000000000..c466b85f2 --- /dev/null +++ b/components/seminar/AdminFeatures.tsx @@ -0,0 +1,20 @@ +import Link from 'next/link'; + +import { seminar } from '@/types/page'; + +import { getPath } from '@/utils/page'; + +const seminarPath = getPath(seminar); + +export default function AdminFeatures() { + return ( +
    + + 새 게시글 + +
    + ); +} diff --git a/components/seminar/EditSeminarPageContent.tsx b/components/seminar/EditSeminarPageContent.tsx index 3ae97bcce..2f604580a 100644 --- a/components/seminar/EditSeminarPageContent.tsx +++ b/components/seminar/EditSeminarPageContent.tsx @@ -3,63 +3,93 @@ import { useRouter } from 'next/navigation'; import { deleteNotice } from '@/apis/notice'; +import { editSeminar } from '@/apis/seminar'; import PageLayout from '@/components/layout/pageLayout/PageLayout'; -import { news } from '@/types/page'; -import { SeminarPostResponse } from '@/types/post'; +import { seminar } from '@/types/page'; +import { Seminar } from '@/types/seminar'; import { getPath } from '@/utils/page'; +import { isLocalFile, isLocalImage, isUploadedFile } from '../editor/PostEditorProps'; import SeminarEditor from '../editor/SeminarEditor'; import { SeminarEditorContent, getSeminarEditorDefaultValue } from '../editor/SeminarEditorProps'; -const newsPath = getPath(news); +const seminarPath = getPath(seminar); -export default function EditSeminarPageContent({ - id, - data, -}: { - id: number; - data: SeminarPostResponse; -}) { +export default function EditSeminarPageContent({ id, data }: { id: number; data: Seminar }) { const router = useRouter(); + const initialContent: SeminarEditorContent = { ...getSeminarEditorDefaultValue(), title: data.title, - description: data.description, + description: data.description ?? '', location: data.location, schedule: { startDate: new Date(data.startDate), - endDate: new Date(data.endDate), + endDate: data.endDate !== null ? new Date(data.endDate) : null, }, speaker: { - name: data.name, - nameURL: data.speakerUrl, + name: data.name ?? '', + nameURL: data.speakerURL, title: data.title, - organization: data.affiliation, - organizationURL: data.affiliationUrl, - description: data.introduction, + organization: data.affiliation ?? '', + organizationURL: data.affiliationURL, + description: data.introduction ?? '', image: data.imageURL ? { type: 'UPLOADED_IMAGE', url: data.imageURL } : null, }, attachments: data.attachments.map((file) => ({ type: 'UPLOADED_FILE', file })), - isPublic: data.isPublic, + isPrivate: data.isPrivate, isImportant: data.isImportant, }; const handleComplete = async (content: SeminarEditorContent) => { throwIfCantSubmit(content); - // const uploadedAttachments = content.attachments.filter(isUploadedFile).map((x) => x.file); - // const localAttachments = content.attachments.filter(isLocalFile).map((x) => x.file); - - router.replace(`${newsPath}/${id}`); + const localAttachments = content.attachments.filter(isLocalFile).map((x) => x.file); + const uploadedAttachments = content.attachments.filter(isUploadedFile).map((x) => x.file); + + const deleteIds = data.attachments + .map((x) => x.id) + .filter((id1) => uploadedAttachments.find((x) => x.id === id1) === undefined); + + const image = + content.speaker.image && isLocalImage(content.speaker.image) + ? content.speaker.image.file + : null; + + await editSeminar(id, { + request: { + title: content.title, + titleForMain: content.titleForMain, + description: emptyStringToNull(content.description), + introduction: emptyStringToNull(content.speaker.description), + name: emptyStringToNull(content.speaker.name), + speakerURL: emptyStringToNull(content.speaker.nameURL), + speakerTitle: emptyStringToNull(content.speaker.title), + affiliation: emptyStringToNull(content.speaker.organization), + affiliationURL: emptyStringToNull(content.speaker.organizationURL), + startDate: content.schedule.startDate.toISOString(), + endDate: content.schedule.endDate?.toISOString() ?? null, + location: content.location, + host: emptyStringToNull(content.host), + isPrivate: content.isPrivate, + isImportant: content.isImportant, + + deleteIds, + }, + image, + newAttachments: localAttachments, + }); + + router.replace(`${seminarPath}/${id}`); }; const handleDelete = async () => { await deleteNotice(id); - router.replace(newsPath); + router.replace(seminarPath); }; return ( @@ -85,14 +115,4 @@ const throwIfCantSubmit = (content: SeminarEditorContent) => { } }; -const strToDate = (dateStr: string, timeStr: string): Date => { - const date = new Date(); - const [y, m, d] = dateStr.split('-').map((x) => +x); - const [h, mm] = timeStr.split(':').map((x) => +x); - date.setFullYear(y); - date.setMonth(m - 1); - date.setDate(d); - date.setHours(h); - date.setMinutes(mm); - return date; -}; +const emptyStringToNull = (str: string | null) => (str ? str : null); diff --git a/components/seminar/SearchBar.tsx b/components/seminar/SearchBar.tsx index 267405a90..5e7e006fc 100644 --- a/components/seminar/SearchBar.tsx +++ b/components/seminar/SearchBar.tsx @@ -5,11 +5,12 @@ import { useState, ChangeEventHandler, FormEvent } from 'react'; import { SearchInfo } from '@/hooks/useCustomSearchParams'; interface SeminarSearchBarProps { + keyword?: string; setSearchParams: (searchInfo: SearchInfo) => void; } -export default function SeminarSearchBar({ setSearchParams }: SeminarSearchBarProps) { - const [text, setText] = useState(''); +export default function SeminarSearchBar({ keyword, setSearchParams }: SeminarSearchBarProps) { + const [text, setText] = useState(keyword ?? ''); const handleChange: ChangeEventHandler = (e) => { setText(e.target.value); diff --git a/components/seminar/SeminarPageContent.tsx b/components/seminar/SeminarPageContent.tsx index fd58d8e6e..e64a58112 100644 --- a/components/seminar/SeminarPageContent.tsx +++ b/components/seminar/SeminarPageContent.tsx @@ -8,16 +8,15 @@ import SeminarYear from '@/components/seminar/SeminarYear'; import { useCustomSearchParams } from '@/hooks/useCustomSearchParams'; -import { GETSeminarPostsResponse } from '@/types/post'; +import { SeminarList } from '@/types/seminar'; + +import AdminFeatures from './AdminFeatures'; +import LoginStaffVisible from '../common/LoginStaffVisible'; const postsCountPerPage = 10; -export default function SeminarContent({ - data: { searchList, total }, -}: { - data: GETSeminarPostsResponse; -}) { - const { page, setSearchParams } = useCustomSearchParams(); +export default function SeminarContent({ data: { searchList, total } }: { data: SeminarList }) { + const { page, keyword, setSearchParams } = useCustomSearchParams(); const setCurrentPage = (pageNum: number) => { setSearchParams({ purpose: 'navigation', pageNum }); @@ -29,9 +28,12 @@ export default function SeminarContent({

    검색

    - +
    + {searchList.length === 0 && ( +

    검색 결과가 존재하지 않습니다.

    + )} {searchList.map((post, index) => (
    {post.isYearLast && } @@ -54,6 +56,9 @@ export default function SeminarContent({ currentPage={page} setCurrentPage={setCurrentPage} /> + + + ); } diff --git a/components/seminar/SeminarRow.tsx b/components/seminar/SeminarRow.tsx index 991226635..cb7335f00 100644 --- a/components/seminar/SeminarRow.tsx +++ b/components/seminar/SeminarRow.tsx @@ -15,6 +15,7 @@ export interface SeminarRowProps { location: string; imageURL: string | null; isYearLast: boolean; + hideDivider?: boolean; } const seminarPath = getPath(seminar); @@ -28,11 +29,12 @@ export default function SeminarRow({ location, imageURL, isYearLast, + hideDivider, }: SeminarRowProps) { return (
    - +
    diff --git a/contexts/SessionContext.tsx b/contexts/SessionContext.tsx index d910dc5f9..cea5ba8dc 100644 --- a/contexts/SessionContext.tsx +++ b/contexts/SessionContext.tsx @@ -1,9 +1,9 @@ 'use client'; -import React, { createContext, PropsWithChildren, useCallback, useContext, useMemo } from 'react'; +import React, { createContext, PropsWithChildren, useContext, useMemo } from 'react'; import useSWR from 'swr'; -import { getRequest, getRequestWithCookie } from '@/apis'; +import { getRequestWithCookie } from '@/apis'; interface SessionContextData { user?: User; diff --git a/data/about.ts b/data/about.ts index 5f6814098..05ff039e0 100644 --- a/data/about.ts +++ b/data/about.ts @@ -62,7 +62,7 @@ export const getMockFacilities = async (): Promise => { export const getMockContact = async (): Promise => { return { description: - '

    서울대학교 공과대학 컴퓨터공학부

    08826 서울특별시 관악구 관악로 1 (301동 316호)


    학부 행정실


    위치: 서울대학교 관악 301동[신공학관1]

    전화: 행정직원에 문의

    팩스: (02) 886-7589

    근무 시간: 평일 9:00 ~ 18:00 / 휴게시간: 12:00 ~ 13:00


    이메일


    학부/대학원 입학 문의: ipsi@cse.snu.ac.kr

    외국인(글로벌전형) 입학 문의: admission@cse.snu.ac.kr

    채용정보게시요청: bulletin@cse.snu.ac.kr

    ', + '

    서울대학교 공과대학 컴퓨터공학부

    08826 서울특별시 관악구 관악로 1 (301동 316호)


    학부 행정실


    위치: 서울대학교 관악 301동[신공학관1]

    전화: 행정직원에 문의

    팩스: (02) 886-7589

    근무 시간: 평일 9:00 ~ 18:00 / 휴게시간: 12:00 ~ 13:00


    이메일


    학부/대학원 입학 문의: ipsi@cse.snu.ac.kr

    외국인(글로벌전형) 입학 문의: admission@cse.snu.ac.kr

    채용정보게시요청: bulletin@cse.snu.ac.kr

    ', imageURL: 'https://cse.snu.ac.kr/sites/default/files/styles/scale-width-220/public/node--contact/301.jpg?itok=zbUgVCfd', }; diff --git a/data/news.ts b/data/news.ts index 0ef3ca889..b92565f98 100644 --- a/data/news.ts +++ b/data/news.ts @@ -27,7 +27,7 @@ export const getMockNewsPostDetail = async (id: number, params: PostSearchQueryP title: `id가 ${id}인 글`, description: htmlMock1, tags: ['연구', '테스트'], - isPublic: true, + isPrivate: false, isSlide: true, imageURL: 'https://picsum.photos/id/237/320/240', createdAt: new Date().toISOString(), diff --git a/data/notice.ts b/data/notice.ts index 214c80113..b987903bf 100644 --- a/data/notice.ts +++ b/data/notice.ts @@ -20,7 +20,7 @@ const noticeDetailMock: Notice = { prevTitle: null, tags: ['장학', '다전공'], isPinned: false, - isPublic: true, + isPrivate: false, isImportant: false, author: 'AUTHOR', description: `

    Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum

    `, @@ -36,7 +36,7 @@ const noticeMockLong: NoticePreview = { createdAt: '2023-07-11T09:29:13', isPinned: true, hasAttachment: true, - isPublic: true, + isPrivate: true, }; const noticeMockPin = { @@ -44,7 +44,7 @@ const noticeMockPin = { createdAt: '2023-07-11T09:29:13', isPinned: true, hasAttachment: false, - isPublic: true, + isPrivate: true, }; const noticeMockPrivate = { @@ -53,14 +53,14 @@ const noticeMockPrivate = { createdAt: '2023-07-11T09:29:13', isPinned: true, hasAttachment: false, - isPublic: false, + isPrivate: false, }; const noticeMock = { title: '2023학년도 2학기 푸른등대 기부장학사업 신규장학생 선발', createdAt: '2023-07-11T09:29:13', isPinned: false, - isPublic: true, + isPrivate: true, hasAttachment: true, }; diff --git a/data/objects.ts b/data/objects.ts index fd8610463..4813f2f1f 100644 --- a/data/objects.ts +++ b/data/objects.ts @@ -277,8 +277,7 @@ export const facilities: Facilities = { name: `학부 행정실`, description: `

    컴퓨터공학부 행정실에서는 학부생, 대학원생, 교수를 위한 다양한 행정 업무를 돕고 있다. 각 업무별 담당자는 직원 목록을 참조.

    `, location: `301동 316호`, - imageURL: - 'https://cse.snu.ac.kr/sites/default/files/styles/medium-landscape-crop/public/node--facility/admin.JPG?itok=-ilXd4wR', + imageURL: '행정실', }, { id: 1, @@ -286,8 +285,7 @@ export const facilities: Facilities = { description: '

    S-Lab은 학생들이 학습, 개발, 토론 등 다양한 목적으로 사용할 수 있는 공간이다. 안쪽에는 중앙의 공간과 분리된 4개의 회의실이 있다. 고성능 PC와 Mac, 스마트 TV, 빔 프로젝터 등의 장비와 회의실을 갖추고 있다.

    ', location: '301동 315호', - imageURL: - 'https://cse.snu.ac.kr/sites/default/files/styles/medium-larger/public/node--facility/IMG_5546.jpg?itok=NvCi07VP', + imageURL: 'SLAB', }, { id: 2, @@ -295,8 +293,7 @@ export const facilities: Facilities = { description: '

    소프트웨어와 관련된 실습 수업 시에 사용된다. 수업 시간이 아닌 경우에는 컴퓨터공학부 학생이라면 누구나 자유롭게 사용할 수 있다. 과거에는 "NT실"이라고 불리기도 했다.

    ', location: '302동 311-1호', - imageURL: - 'https://cse.snu.ac.kr/sites/default/files/styles/medium-larger/public/node--facility/1.%EC%86%8C%ED%94%84%ED%8A%B8%EC%9B%A8%EC%96%B4%EC%8B%A4%EC%8A%B5%EC%8B%A4.jpg?itok=SVfNrk8b', + imageURL: '소프트웨어', }, { id: 3, @@ -304,8 +301,7 @@ export const facilities: Facilities = { description: '

    하드웨어 관련 실습 수업에 사용된다. 오실로스코프, 직류 전원 공급기, 함수 발생기, 멀티미터, Intel Core i7 PC 등의 장비와 각종 공구를 이용할 수 있다. 논리설계나 하드웨어시스템설계 등의 수업을 수강하는 학생들은 이곳에서 많은 시간을 보내게 된다.

    ', location: '302동 310-2호', - imageURL: - 'https://cse.snu.ac.kr/sites/default/files/styles/medium-larger/public/node--facility/hw_lab_0.jpg?itok=P53mcJL5', + imageURL: '하드웨어', }, { id: 3, @@ -313,8 +309,7 @@ export const facilities: Facilities = { description: '

    해동학술정보실은 전기공학부와 컴퓨터공학부 학생들을 위한 도서관이다. 정기 간행물 및 논문을 열람하거나 대여할 수 있다. 조용한 환경에서 공부할 수 있는 230석 규모의 열람실이 딸려 있다.

    ', location: '310동 312호 ', - imageURL: - 'https://cse.snu.ac.kr/sites/default/files/styles/medium-larger/public/node--facility/haedong_0.JPG?itok=O_2aXCK2', + imageURL: '해동', }, { id: 4, @@ -322,8 +317,7 @@ export const facilities: Facilities = { description: '

    과방은 학부생들의 주 생활 공간이다. 끊임없이 나오는 과제를 해치우는데 사용되는 수십 대의 Intel Core i7 PC가 구비되어 있으며, 오랫동안 컴퓨터를 하느라 편할 날이 없는 학생들의 눈·목·손목 등의 휴식을 위한 편의 시설도 마련되어 있다. 학생들이 기증하거나 잠시 빌려준 각종 서적·만화책·보드 게임 등이 있어 공부뿐만 아니라 여가도 즐길 줄 아는 학생들을 위한 환경이 조성되어 있다.

    ', location: '301동 315호', - imageURL: - 'https://cse.snu.ac.kr/sites/default/files/styles/medium-landscape-crop/public/node--facility/301%EB%8F%99%20314%ED%98%B8%20%EA%B3%BC%EB%B0%A9.jpg?itok=p36Nt-sl', + imageURL: '과방', }, { id: 5, @@ -332,8 +326,7 @@ export const facilities: Facilities = { '

    세미나실은 301동과 302동에 있다. 컴퓨터공학부 대학원생들이 온라인예약하여 사용할 수 있다.

    ', location: '301동 417호, 301동 521호(MALDIVES), 301동 551-4호(HAWAII), 302동 308호, 302동 309-1호, 302동 309-2호, 302동 309-3호', - imageURL: - 'https://cse.snu.ac.kr/sites/default/files/styles/medium-larger/public/node--facility/302%EB%8F%99309-1%ED%98%B8_20190301.jpg?itok=dww7DhCf', + imageURL: '세미나실', }, { id: 5, @@ -341,8 +334,7 @@ export const facilities: Facilities = { description: '

    컴퓨터공학부의 실습 서버, 통합계정 서버, 프린터 서버 등 각종 서버 및 워크스테이션을 관리하는 곳이다. 학부 서버는 학생 동아리인 바쿠스에서 관리하고 있다.

    ', location: '302동 310-2호', - imageURL: - 'https://cse.snu.ac.kr/sites/default/files/styles/medium-larger/public/node--facility/%EC%84%9C%EB%B2%84%EC%8B%A4.JPG?itok=_7gF4r5h', + imageURL: '서버실', }, ], }; @@ -351,7 +343,7 @@ export const directions: Direction[] = [ { name: '대중교통', engName: 'public-transit', - description: `

    지하철 2호선 낙성대역

    낙성대역 4번 출구로 나와 직진, 주유소에서 좌회전하여 제과점 앞 정류장에서 마을버스 관악02를 타고 제2공학관에서 내립니다.


    지하철 2호선 서울대입구역

    서울대입구역 3번 출구로 나와 관악구청 방향으로 직진하여 학교 셔틀 버스나 시내버스 5511 또는 5513을 타고 제2공학관에서 내립니다. 제2공학관행 셔틀 버스는 아침 8시부터 10시까지 15분 간격으로 월요일부터 금요일까지 운행됩니다.


    지하철 2호선 신림역

    신림역 3번 출구에서 나와 시내버스 5516을 타고 제2공학관에서 하차합니다.


    지하철 신림경전철 관악산역

    관악산역 1번 출구로 나와 직진, 관악산입구.관악아트홀.중앙도서관 정류장에서 5511 또는 5516 시내버스를 타고 제2공학관에서 내립니다.


    서울대학교 정문 경유 버스

    서울대학교 정문을 경유하는 버스를 이용해서 정문에 내린 후 교내순환 셔틀버스, 시내버스 5511 또는 5513을 타고 제2공학관으로 갈 수 있습니다.

    버스 번호
    종점
    경유지
    5517
    시흥벽산APT ↔ 중앙대
    서울대, 노량진
    6511
    서울대 ↔ 구로동
    신대방역, 신도림역
    6512
    서울대 ↔ 구로동
    서울대입구역, 신림역, 영등포
    6513
    서울대 ↔ 철산동
    신림역, 대방역, 영등포
    750A, 750B
    서울대 ↔ 덕은동
    관악구청, 상도터널, 서울역, 신촌, 수색역
    5528
    가산동 ↔ 사당동
    금천경찰서, 신림사거리, 서울대, 낙성대
    6514
    서울대 ↔ 양천
    신림역, 대방역, 영등포역, 당산역
    501
    서울대 ↔ 종로2가
    상도동, 신용산, 서울역
    6003
    서울대 ↔ 인천공항
    대림역, 목동역, 88체육관, 김포공항
    `, + description: `

    지하철 2호선 낙성대역

    낙성대역 4번 출구로 나와 직진, 주유소에서 좌회전하여 제과점 앞 정류장에서 마을버스 관악02를 타고 제2공학관에서 내립니다.


    지하철 2호선 서울대입구역

    서울대입구역 3번 출구로 나와 관악구청 방향으로 직진하여 학교 셔틀 버스나 시내버스 5511 또는 5513을 타고 제2공학관에서 내립니다. 제2공학관행 셔틀 버스는 아침 8시부터 10시까지 15분 간격으로 월요일부터 금요일까지 운행됩니다.


    지하철 2호선 신림역

    신림역 3번 출구에서 나와 시내버스 5516을 타고 제2공학관에서 하차합니다.


    지하철 신림경전철 관악산역

    관악산역 1번 출구로 나와 직진, 관악산입구.관악아트홀.중앙도서관 정류장에서 5511 또는 5516 시내버스를 타고 제2공학관에서 내립니다.


    서울대학교 정문 경유 버스

    서울대학교 정문을 경유하는 버스를 이용해서 정문에 내린 후 교내순환 셔틀버스, 시내버스 5511 또는 5513을 타고 제2공학관으로 갈 수 있습니다.

    버스 번호
    종점
    경유지
    5517
    시흥벽산APT ↔ 중앙대
    서울대, 노량진
    6511
    서울대 ↔ 구로동
    신대방역, 신도림역
    6512
    서울대 ↔ 구로동
    서울대입구역, 신림역, 영등포
    6513
    서울대 ↔ 철산동
    신림역, 대방역, 영등포
    750A, 750B
    서울대 ↔ 덕은동
    관악구청, 상도터널, 서울역, 신촌, 수색역
    5528
    가산동 ↔ 사당동
    금천경찰서, 신림사거리, 서울대, 낙성대
    6514
    서울대 ↔ 양천
    신림역, 대방역, 영등포역, 당산역
    501
    서울대 ↔ 종로2가
    상도동, 신용산, 서울역
    6003
    서울대 ↔ 인천공항
    대림역, 목동역, 88체육관, 김포공항

    `, }, { name: '승용차', diff --git a/data/reservation.ts b/data/reservation.ts index f0a7c2cd9..f8cea3e24 100644 --- a/data/reservation.ts +++ b/data/reservation.ts @@ -14,7 +14,7 @@ export const getMockWeeklyReservation = async ( const endDateTime = endDate.getTime(); return reservations.filter((x) => { - const time = x.startTime.getTime(); + const time = new Date(x.startTime).getTime(); return startDateTime <= time && time <= endDateTime; }); }; @@ -36,8 +36,8 @@ const buildMockReservation = (id: number, startTime: Date, endTime: Date): Reser purpose: '목적', - startTime, - endTime, + startTime: startTime.toISOString(), + endTime: endTime.toISOString(), recurringWeeks: 1, title: '예약 1', diff --git a/data/seminar.ts b/data/seminar.ts index 4eb2816be..329d9ad15 100644 --- a/data/seminar.ts +++ b/data/seminar.ts @@ -55,7 +55,7 @@ export const getMockSeminarPost = async (id: number) => { nextTitle: 'NEXTTITLE', createdAt: new Date().toISOString(), modifiedAt: new Date().toISOString(), - isPublic: true, + isPrivate: true, isSlide: true, additionalNote: '', }; diff --git a/messages/en.json b/messages/en.json index fa56f8758..ea9e77682 100644 --- a/messages/en.json +++ b/messages/en.json @@ -18,8 +18,8 @@ "연락처": "Contact Us", "찾아오는 길": "Directions", - "공지사항": "News", - "새 소식": "Notice", + "공지사항": "Notice", + "새 소식": "News", "세미나": "Seminars", "신임교수초빙": "Faculty Recruitment", @@ -78,7 +78,12 @@ "Participants(Professors)": "Participants(Professors)", "Proposal": "Proposal", - "관련 페이지": "관련 페이지" + "관련 페이지": "관련 페이지", + + "검색": "Keyword", + "통합 검색": "Search", + "결과 더보기": "more results", + "전체": "Total" }, "Header": { diff --git a/messages/ko.json b/messages/ko.json index 671a860af..e13267119 100644 --- a/messages/ko.json +++ b/messages/ko.json @@ -79,7 +79,12 @@ "Participants(Professors)": "Participants(Professors)", "Proposal": "Proposal", - "관련 페이지": "관련 페이지" + "관련 페이지": "관련 페이지", + + "검색": "검색", + "통합 검색": "통합 검색", + "결과 더보기": "결과 더보기", + "전체": "전체" }, "Header": { diff --git a/next.config.js b/next.config.js index 80321707e..16b0810d1 100644 --- a/next.config.js +++ b/next.config.js @@ -52,6 +52,12 @@ const nextConfig = { hostname: 'cse-dev-waffle.bacchus.io', pathname: '/**', }, + { + protocol: 'http', + hostname: 'localhost', + pathname: '/**', + port: '8080', + }, ], }, diff --git a/public/image/facilities/SLAB-color.png b/public/image/facilities/SLAB-color.png new file mode 100644 index 000000000..e6cc3d8ea Binary files /dev/null and b/public/image/facilities/SLAB-color.png differ diff --git a/public/image/facilities/SLAB-original.png b/public/image/facilities/SLAB-original.png new file mode 100644 index 000000000..63f27ea27 Binary files /dev/null and b/public/image/facilities/SLAB-original.png differ diff --git "a/public/image/facilities/\352\263\274\353\260\251-color.png" "b/public/image/facilities/\352\263\274\353\260\251-color.png" new file mode 100644 index 000000000..354e84800 Binary files /dev/null and "b/public/image/facilities/\352\263\274\353\260\251-color.png" differ diff --git "a/public/image/facilities/\352\263\274\353\260\251-original.png" "b/public/image/facilities/\352\263\274\353\260\251-original.png" new file mode 100644 index 000000000..13e6d3c21 Binary files /dev/null and "b/public/image/facilities/\352\263\274\353\260\251-original.png" differ diff --git "a/public/image/facilities/\354\204\234\353\262\204\354\213\244-color.png" "b/public/image/facilities/\354\204\234\353\262\204\354\213\244-color.png" new file mode 100644 index 000000000..b419e5cef Binary files /dev/null and "b/public/image/facilities/\354\204\234\353\262\204\354\213\244-color.png" differ diff --git "a/public/image/facilities/\354\204\234\353\262\204\354\213\244-original.png" "b/public/image/facilities/\354\204\234\353\262\204\354\213\244-original.png" new file mode 100644 index 000000000..1040d008d Binary files /dev/null and "b/public/image/facilities/\354\204\234\353\262\204\354\213\244-original.png" differ diff --git "a/public/image/facilities/\354\204\270\353\257\270\353\202\230\354\213\244-color.png" "b/public/image/facilities/\354\204\270\353\257\270\353\202\230\354\213\244-color.png" new file mode 100644 index 000000000..30f19780b Binary files /dev/null and "b/public/image/facilities/\354\204\270\353\257\270\353\202\230\354\213\244-color.png" differ diff --git "a/public/image/facilities/\354\204\270\353\257\270\353\202\230\354\213\244-original.png" "b/public/image/facilities/\354\204\270\353\257\270\353\202\230\354\213\244-original.png" new file mode 100644 index 000000000..e68dd8eae Binary files /dev/null and "b/public/image/facilities/\354\204\270\353\257\270\353\202\230\354\213\244-original.png" differ diff --git "a/public/image/facilities/\354\206\214\355\224\204\355\212\270\354\233\250\354\226\264-color.png" "b/public/image/facilities/\354\206\214\355\224\204\355\212\270\354\233\250\354\226\264-color.png" new file mode 100644 index 000000000..42d8d0df0 Binary files /dev/null and "b/public/image/facilities/\354\206\214\355\224\204\355\212\270\354\233\250\354\226\264-color.png" differ diff --git "a/public/image/facilities/\354\206\214\355\224\204\355\212\270\354\233\250\354\226\264-original.png" "b/public/image/facilities/\354\206\214\355\224\204\355\212\270\354\233\250\354\226\264-original.png" new file mode 100644 index 000000000..088cfcbb9 Binary files /dev/null and "b/public/image/facilities/\354\206\214\355\224\204\355\212\270\354\233\250\354\226\264-original.png" differ diff --git "a/public/image/facilities/\355\225\230\353\223\234\354\233\250\354\226\264-color.png" "b/public/image/facilities/\355\225\230\353\223\234\354\233\250\354\226\264-color.png" new file mode 100644 index 000000000..d440a9f59 Binary files /dev/null and "b/public/image/facilities/\355\225\230\353\223\234\354\233\250\354\226\264-color.png" differ diff --git "a/public/image/facilities/\355\225\230\353\223\234\354\233\250\354\226\264-original.png" "b/public/image/facilities/\355\225\230\353\223\234\354\233\250\354\226\264-original.png" new file mode 100644 index 000000000..0c6100c75 Binary files /dev/null and "b/public/image/facilities/\355\225\230\353\223\234\354\233\250\354\226\264-original.png" differ diff --git "a/public/image/facilities/\355\225\264\353\217\231-color.png" "b/public/image/facilities/\355\225\264\353\217\231-color.png" new file mode 100644 index 000000000..ff6c2c677 Binary files /dev/null and "b/public/image/facilities/\355\225\264\353\217\231-color.png" differ diff --git "a/public/image/facilities/\355\225\264\353\217\231-original.png" "b/public/image/facilities/\355\225\264\353\217\231-original.png" new file mode 100644 index 000000000..76b739b68 Binary files /dev/null and "b/public/image/facilities/\355\225\264\353\217\231-original.png" differ diff --git "a/public/image/facilities/\355\226\211\354\240\225\354\213\244-color.png" "b/public/image/facilities/\355\226\211\354\240\225\354\213\244-color.png" new file mode 100644 index 000000000..3ca8bb71a Binary files /dev/null and "b/public/image/facilities/\355\226\211\354\240\225\354\213\244-color.png" differ diff --git "a/public/image/facilities/\355\226\211\354\240\225\354\213\244-original.png" "b/public/image/facilities/\355\226\211\354\240\225\354\213\244-original.png" new file mode 100644 index 000000000..424e2d634 Binary files /dev/null and "b/public/image/facilities/\355\226\211\354\240\225\354\213\244-original.png" differ diff --git a/styles/font.ts b/styles/font.ts index f78bbdebb..347be4e23 100644 --- a/styles/font.ts +++ b/styles/font.ts @@ -2,7 +2,7 @@ import { Noto_Sans_KR } from 'next/font/google'; import localFont from 'next/font/local'; export const noto = Noto_Sans_KR({ - weight: ['300', '400', '500', '700'], + weight: ['300', '500', '700'], style: ['normal'], subsets: ['latin'], display: 'swap', diff --git a/types/news.ts b/types/news.ts index 4441b6677..86387a952 100644 --- a/types/news.ts +++ b/types/news.ts @@ -4,7 +4,7 @@ export interface NewsPreview { description: string; tags: string[]; createdAt: string; - isPublic: boolean; + isPrivate: boolean; imageURL: string | null; } @@ -16,7 +16,7 @@ export interface NewsPreviewList { export interface News { title: string; description: string; - isPublic: boolean; + isPrivate: boolean; tags: string[]; imageURL: string | null; isSlide: boolean; @@ -30,6 +30,7 @@ export interface News { nextId: number | null; nextTitle: string | null; attachments: { + id: number; name: string; url: string; bytes: number; @@ -39,8 +40,9 @@ export interface News { export interface POSTNewsBody { request: { title: string; + titleForMain: string | null; description: string; - isPublic: boolean; + isPrivate: boolean; isSlide: boolean; isImportant: boolean; tags: string[]; @@ -52,16 +54,13 @@ export interface POSTNewsBody { export interface PATCHNewsBody { request: { title: string; + titleForMain: string | null; description: string; - isPublic: boolean; + isPrivate: boolean; isSlide: boolean; isImportant: boolean; tags: string[]; - attachments: { - name: string; - url: string; - bytes: number; - }[]; + deleteIds: number[]; }; mainImage: File | null; newAttachments: File[]; diff --git a/types/notice.ts b/types/notice.ts index 0ebdb5ed5..76a042ef9 100644 --- a/types/notice.ts +++ b/types/notice.ts @@ -4,7 +4,7 @@ export interface NoticePreview { isPinned: boolean; createdAt: string; hasAttachment: boolean; - isPublic: boolean; + isPrivate: boolean; } export interface NoticePreviewList { @@ -15,7 +15,7 @@ export interface NoticePreviewList { export interface Notice { title: string; description: string; - isPublic: boolean; + isPrivate: boolean; tags: string[]; isPinned: boolean; isImportant: boolean; @@ -29,6 +29,7 @@ export interface Notice { nextId: number | null; nextTitle: string | null; attachments: { + id: number; name: string; url: string; bytes: number; @@ -38,8 +39,9 @@ export interface Notice { export interface POSTNoticeBody { request: { title: string; + titleForMain: string | null; description: string; - isPublic: boolean; + isPrivate: boolean; isPinned: boolean; isImportant: boolean; tags: string[]; @@ -50,16 +52,13 @@ export interface POSTNoticeBody { export interface PatchNoticeBody { request: { title: string; + titleForMain: string | null; description: string; - isPublic: boolean; + isPrivate: boolean; isPinned: boolean; isImportant: boolean; tags: string[]; - attachments: { - name: string; - url: string; - bytes: number; - }[]; + deleteIds: number[]; }; newAttachments: File[]; } diff --git a/types/post.ts b/types/post.ts index 9ee5e5593..82136059f 100644 --- a/types/post.ts +++ b/types/post.ts @@ -5,7 +5,7 @@ export interface Post { title: string; // html 내용 description: string; - isPublic: boolean; + isPrivate: boolean; attachments: FormData; } @@ -52,96 +52,3 @@ export interface GETFacultyRecruitmentResponse { latestRecruitmentPostHref: string; description: string; } - -// 세미나 - - - - - - - - - - - - - - - - - - - - - - - -export interface GETSeminarPostsResponse { - total: number; - searchList: SimpleSeminarPost[]; -} - -export interface SimpleSeminarPost - extends Pick< - SeminarPostResponse, - 'id' | 'title' | 'name' | 'affiliation' | 'startDate' | 'location' | 'imageURL' - > { - isYearLast: boolean; -} - -export interface POSTSeminarBody { - request: { - title: string; - description: string; - introduction: string; - name: string; - speakerURL: string | null; - speakerTitle: string | null; - affiliation: string; - affiliationURL: string | null; - startDate: string | null; - endDate: string | null; - location: string; - host: string | null; - isPublic: boolean; - isImportant: boolean; - }; - image: File | null; - attachments: File[]; -} - -export interface SeminarPostResponse { - id: number; - title: string; - description: string; - isPublic: boolean; - - createdAt: string; - modifiedAt: string; - prevId: number | null; - prevTitle: string | null; - nextId: number | null; - nextTitle: string | null; - - introduction: string; - category: string; - name: string; - speakerUrl: string | null; - speakerTitle: string | null; - affiliation: string; - affiliationUrl: string | null; - startDate: string; - endDate: string; - location: string; - host: string; - isImportant: boolean; - imageURL: string | null; - attachments: { - name: string; - url: string; - bytes: number; - }[]; -} - -export interface PatchSeminarBody { - request: { - introduction: string; - category: string; - name: string; - speakerUrl: string | null; - speakerTitle: string | null; - affiliation: string; - affiliationUrl: string | null; - startDate: string | null; - endDate: string | null; - location: string; - host: string | null; - isSlide: boolean; - attachments: { - name: string; - url: string; - bytes: number; - }[]; - }; - newAttachments: File[]; - image: File | null; -} diff --git a/types/reservation.ts b/types/reservation.ts index c22a0e914..d7359c7be 100644 --- a/types/reservation.ts +++ b/types/reservation.ts @@ -1,9 +1,16 @@ +export interface ReservationPreview { + id: number; + title: string; + startTime: string; + endTime: string; +} + export interface Reservation { id: number; recurrenceId: string; - startTime: Date; - endTime: Date; + startTime: string; + endTime: string; recurringWeeks: number; title: string; @@ -22,8 +29,8 @@ export interface Reservation { export interface ReservationPostBody { roomId: number; - startTime: Date; - endTime: Date; + startTime: string; + endTime: string; recurringWeeks: number; title: string; diff --git a/types/search.ts b/types/search.ts new file mode 100644 index 000000000..2a9c9c0f6 --- /dev/null +++ b/types/search.ts @@ -0,0 +1,26 @@ +interface NoticeSearchResult { + total: number; + results: { + id: number; + title: string; + createdAt: string; + partialDescription: string; + boldStartIndex: number; + boldEndIndex: number; + }[]; +} + +interface NewsSearchResult { + total: number; + results: { + id: number; + title: string; + date: string; + partialDescription: string; + boldStartIndex: number; + boldEndIndex: number; + tags: string[]; + imageUrl: string | null; + }[]; +} + diff --git a/types/seminar.ts b/types/seminar.ts index 98b0496e3..35fb37527 100644 --- a/types/seminar.ts +++ b/types/seminar.ts @@ -1,96 +1,95 @@ -export interface SeminarPreview { - id: number; - title: string; - description: string; - name: string; - affiliation: string; - startDate: string; - location: string; - imageURL: string | null; - isYearLast: boolean; -} - export interface SeminarList { total: number; searchList: SeminarPreview[]; } -export interface Seminar { +export interface SeminarPreview { + id: number; title: string; description: string; - isPublic: boolean; - - introduction: string; - category: string; name: string; - speakerUrl: string | null; - speakerTitle: string | null; affiliation: string; - affiliationUrl: string | null; startDate: string; - endDate: string; location: string; - host: string; - isImportant: boolean; imageURL: string | null; + isYearLast: boolean; +} - id: number; - createdAt: string; - modifiedAt: string; - prevId: number | null; - prevTitle: string | null; - nextId: number | null; - nextTitle: string | null; +export interface Seminar { + affiliation: string | null; + affiliationURL: string | null; attachments: { + id: number; name: string; url: string; bytes: number; }[]; + createdAt: string; + description: string | null; + endDate: string | null; + host: string | null; + id: number; + imageURL: string | null; + introduction: string | null; + isImportant: boolean; + isPrivate: boolean; + location: string; + modifiedAt: string; + name: string | null; + nextId: number | null; + nextTitle: string | null; + prevId: number | null; + prevTitle: string | null; + speakerTitle: string | null; + speakerURL: string | null; + startDate: string; + title: string; } export interface POSTSeminarBody { request: { - title: string; - description: string; - introduction: string; - name: string; + title: string | null; + titleForMain: string | null; + description: string | null; + introduction: string | null; + name: string | null; speakerURL: string | null; speakerTitle: string | null; - affiliation: string; + affiliation: string | null; affiliationURL: string | null; - startDate: string | null; + startDate: string; endDate: string | null; location: string; host: string | null; - isPublic: boolean; + isPrivate: boolean; isImportant: boolean; }; + image: File | null; attachments: File[]; } -export interface PatchSeminarBody { +export interface PATCHSeminarBody { request: { - introduction: string; - category: string; - name: string; - speakerUrl: string | null; + title: string | null; + titleForMain: string | null; + description: string | null; + introduction: string | null; + name: string | null; + speakerURL: string | null; speakerTitle: string | null; - affiliation: string; - affiliationUrl: string | null; - startDate: string | null; - startTime: string | null; + affiliation: string | null; + affiliationURL: string | null; + startDate: string; endDate: string | null; - endTime: string | null; location: string; host: string | null; - isSlide: boolean; - attachments: { - name: string; - url: string; - bytes: number; - }[]; + isPrivate: boolean; + isImportant: boolean; + + deleteIds: number[]; }; + newAttachments: File[]; image: File | null; }