diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 6b2a8170..64e74975 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -4,7 +4,7 @@ on: push: branches: - main - - fix/image + - feat/search jobs: build: diff --git a/apis/image.ts b/apis/image.ts new file mode 100644 index 00000000..8a951761 --- /dev/null +++ b/apis/image.ts @@ -0,0 +1,21 @@ +'use server'; + +import { postRequest } from '.'; + +export const postImage = async (formData: FormData) => { + const resp = (await postRequest('/file/upload', { + body: formData, + jsessionID: true, + })) as ImageUploadResponse; + + return resp; +}; + +type ImageUploadResponse = { + errorMessage: string; + result: { + url: string; + name: string; + size: number; + }[]; +}; diff --git a/app/[locale]/community/news/[id]/NewsViewer.tsx b/app/[locale]/community/news/[id]/NewsViewer.tsx index e312a320..1afa15e3 100644 --- a/app/[locale]/community/news/[id]/NewsViewer.tsx +++ b/app/[locale]/community/news/[id]/NewsViewer.tsx @@ -34,7 +34,15 @@ export default async function NewsViewer({ id, searchParams }: NewsPostPageProps style={{ paddingBottom: PAGE_PADDING_BOTTOM_PX }} > - + diff --git a/app/[locale]/community/seminar/SeminarContent.tsx b/app/[locale]/community/seminar/SeminarContent.tsx index 54bf888e..611d6538 100644 --- a/app/[locale]/community/seminar/SeminarContent.tsx +++ b/app/[locale]/community/seminar/SeminarContent.tsx @@ -1,3 +1,5 @@ +import { Fragment } from 'react'; + import { getSeminarPosts } from '@/apis/seminar'; import SeminarRow from '@/app/[locale]/community/seminar/helper/SeminarRow'; @@ -29,10 +31,14 @@ export default async function SeminarContent({ ) : ( searchList.map((post, index) => ( -
+ {post.isYearLast && } - -
+
+ +
+ )) )} diff --git a/app/[locale]/community/seminar/helper/SeminarRow.tsx b/app/[locale]/community/seminar/helper/SeminarRow.tsx index 7f4c21fa..d2fdc95a 100644 --- a/app/[locale]/community/seminar/helper/SeminarRow.tsx +++ b/app/[locale]/community/seminar/helper/SeminarRow.tsx @@ -13,24 +13,18 @@ import { seminar } from '@/utils/segmentNode'; export interface SeminarRowProps { seminar: SeminarPreview; - hideDivider: boolean; } const seminarPath = getPath(seminar); export default function SeminarRow({ - seminar: { id, isYearLast, imageURL, title, name, affiliation, startDate, location }, - hideDivider, + seminar: { id, imageURL, title, name, affiliation, startDate, location }, }: SeminarRowProps) { const seminarPostPath = `${seminarPath}/${id}`; return ( -
+
diff --git a/app/[locale]/search/AboutSection.tsx b/app/[locale]/search/AboutSection.tsx index f30a8735..e0a27948 100644 --- a/app/[locale]/search/AboutSection.tsx +++ b/app/[locale]/search/AboutSection.tsx @@ -1,6 +1,4 @@ -import { searchAbout } from '@/apis/search'; - -import { AboutPreview } from '@/types/search'; +import { AboutPreview, AboutSearchResult } from '@/types/search'; import { getPath } from '@/utils/page'; import { @@ -17,9 +15,7 @@ import { import BasicRow from './helper/BasicRow'; import Section from './helper/Section'; -export default async function AboutSection({ keyword }: { keyword: string }) { - const about = await searchAbout({ keyword, number: 3, amount: 200 }); - +export default async function AboutSection({ about }: { about: AboutSearchResult }) { return (
diff --git a/app/[locale]/search/AcademicSection.tsx b/app/[locale]/search/AcademicSection.tsx index 07cd8c1a..023cf457 100644 --- a/app/[locale]/search/AcademicSection.tsx +++ b/app/[locale]/search/AcademicSection.tsx @@ -1,20 +1,30 @@ -import { searchAcademics } from '@/apis/search'; +import { Academic, AcademicsSearchResult } from '@/types/search'; import { getPath } from '@/utils/page'; -import { undergraduateGuide } from '@/utils/segmentNode'; +import { + curriculum, + degree, + generalStudies, + graduateCourseChanges, + graduateCourses, + graduateGuide, + graduateScholarship, + undergraduateCourseChanges, + undergraduateCourses, + undergraduateGuide, + undergraduateScholarship, +} from '@/utils/segmentNode'; import BasicRow from './helper/BasicRow'; import Section from './helper/Section'; -export default async function AcademicSection({ keyword }: { keyword: string }) { - const academic = await searchAcademics({ keyword, number: 3, amount: 200 }); - +// TODO: 장학 제도 등 상세 페이지로 연결 +export default async function AcademicSection({ academic }: { academic: AcademicsSearchResult }) { return (
{academic.results.map((result) => { - // TODO - const node = undergraduateGuide; + const node = toNode(result); return ( ); } + +const toNode = (academic: Academic) => { + // 공통 + + // 학부/대학원 안내 + if (academic.academicType === 'GUIDE') + return academic.studentType === 'UNDERGRADUATE' ? undergraduateGuide : graduateGuide; + + // 교과과정 + if (academic.postType === 'COURSE') + return academic.studentType === 'UNDERGRADUATE' ? undergraduateCourses : graduateCourses; + + // 교과목 변경 내역 + if (academic.academicType === 'COURSE_CHANGES') + return academic.studentType === 'UNDERGRADUATE' + ? undergraduateCourseChanges + : graduateCourseChanges; + + // 장학 제도 + if (academic.postType === 'SCHOLARSHIP') + return academic.studentType === 'UNDERGRADUATE' + ? undergraduateScholarship + : graduateScholarship; + + // 학부 전용 + + // 전공이수표준형태 + if (academic.academicType === 'CURRICULUM') return curriculum; + + // 필수 교양 과목 + if ( + academic.academicType === 'GENERAL_STUDIES_REQUIREMENTS' || + academic.academicType === 'GENERAL_STUDIES_REQUIREMENTS_SUBJECT_CHANGES' + ) + return generalStudies; + + // 졸업 규정 + if ( + academic.academicType === 'DEGREE_REQUIREMENTS' || + academic.academicType === 'DEGREE_REQUIREMENTS_YEAR_LIST' + ) + return degree; + + // TODO: fallback 없애기 + return undergraduateCourses; +}; diff --git a/app/[locale]/search/AdmissionSection.tsx b/app/[locale]/search/AdmissionSection.tsx index cd702a39..d7ac364b 100644 --- a/app/[locale]/search/AdmissionSection.tsx +++ b/app/[locale]/search/AdmissionSection.tsx @@ -1,4 +1,4 @@ -import { searchAdmissions } from '@/apis/search'; +import { AdmissionsSearchResult } from '@/types/search'; import { getPath } from '@/utils/page'; import { admissions } from '@/utils/segmentNode'; @@ -6,9 +6,11 @@ import { admissions } from '@/utils/segmentNode'; import BasicRow from './helper/BasicRow'; import Section from './helper/Section'; -export default async function AdmissionSection({ keyword }: { keyword: string }) { - const admission = await searchAdmissions({ keyword, number: 3, amount: 200 }); - +export default async function AdmissionSection({ + admission, +}: { + admission: AdmissionsSearchResult; +}) { return (
diff --git a/app/[locale]/search/CommunitySection.tsx b/app/[locale]/search/CommunitySection.tsx index 136fb8f6..70f853f2 100644 --- a/app/[locale]/search/CommunitySection.tsx +++ b/app/[locale]/search/CommunitySection.tsx @@ -2,37 +2,41 @@ import Link from 'next/link'; import { useTranslations } from 'next-intl'; import { ReactNode } from 'react'; -import { searchNews, searchNotice } from '@/apis/search'; -import { getSeminarPosts } from '@/apis/seminar'; +import { NewsSearchResult, NoticeSearchResult } from '@/types/search'; +import { SeminarList } from '@/types/seminar'; import { getPath } from '@/utils/page'; import { news, notice, seminar } from '@/utils/segmentNode'; import CircleTitle from './helper/CircleTitle'; import Divider from './helper/Divider'; +import NewsRow from './helper/NewsRow'; import NoticeRow from './helper/NoticeRow'; import Section from './helper/Section'; -import NewsRow from '../community/news/helper/NewsRow'; import SeminarRow from '../community/seminar/helper/SeminarRow'; const newsPath = getPath(news); const noticePath = getPath(notice); const seminarPath = getPath(seminar); -export default async function CommunitySection({ keyword }: { keyword: string }) { - const [notice, news, seminar] = await Promise.all([ - searchNotice({ keyword, number: 3, amount: 200 }), - searchNews({ keyword, number: 3, amount: 200 }), - getSeminarPosts({ keyword, pageNum: '1' }), - ]); - +export default async function CommunitySection({ + keyword, + notice, + news, + seminar, +}: { + keyword: string; + notice: NoticeSearchResult; + news: NewsSearchResult; + seminar: SeminarList; +}) { return (
{notice.results.map((notice) => ( {news.results.map((news) => ( ))} @@ -72,7 +73,7 @@ export default async function CommunitySection({ keyword }: { keyword: string }) href={`${seminarPath}?keyword=${keyword}`} > {seminar.searchList.slice(0, 3).map((seminar) => ( - + ))}
@@ -97,7 +98,9 @@ const CommunitySubSection = ({ return ( <> -
{children}
+
+ {children} +
{divider && } diff --git a/app/[locale]/search/MemberSection.tsx b/app/[locale]/search/MemberSection.tsx index a33db3ec..62b1770c 100644 --- a/app/[locale]/search/MemberSection.tsx +++ b/app/[locale]/search/MemberSection.tsx @@ -1,10 +1,8 @@ import Link from 'next/link'; -import { searchMember } from '@/apis/search'; - import ImageWithFallback from '@/components/common/ImageWithFallback'; -import { Member } from '@/types/search'; +import { Member, MemberSearchResult } from '@/types/search'; import { getPath } from '@/utils/page'; import { faculty, staff } from '@/utils/segmentNode'; @@ -13,14 +11,12 @@ import CircleTitle from './helper/CircleTitle'; import Divider from './helper/Divider'; import Section from './helper/Section'; -export default async function MemberSection({ keyword }: { keyword: string }) { - const resp = await searchMember({ keyword, number: 10, amount: 200 }); - - const professorList = resp.results.filter((x) => x.memberType === 'PROFESSOR'); - const staffList = resp.results.filter((x) => x.memberType === 'STAFF'); +export default async function MemberSection({ member }: { member: MemberSearchResult }) { + const professorList = member.results.filter((x) => x.memberType === 'PROFESSOR'); + const staffList = member.results.filter((x) => x.memberType === 'STAFF'); return ( -
+
{professorList.length !== 0 && ( <> @@ -53,7 +49,7 @@ const MemberCell = ({ name, academicRankOrRole, imageURL, memberType, id }: Memb const href = `${memberType === 'PROFESSOR' ? facultyPath : staffPath}/${id}`; return ( - +
-

{name}

+

+ {name} +

{academicRankOrRole}

diff --git a/app/[locale]/search/ResearchSection.tsx b/app/[locale]/search/ResearchSection.tsx index 0c16be4a..c233358f 100644 --- a/app/[locale]/search/ResearchSection.tsx +++ b/app/[locale]/search/ResearchSection.tsx @@ -1,6 +1,4 @@ -import { searchResearch } from '@/apis/search'; - -import { ResearchType } from '@/types/search'; +import { ResearchSearchResult, ResearchType } from '@/types/search'; import { getPath } from '@/utils/page'; import { @@ -13,9 +11,7 @@ import { import BasicRow from './helper/BasicRow'; import Section from './helper/Section'; -export default async function ResearchSection({ keyword }: { keyword: string }) { - const research = await searchResearch({ keyword, number: 3, amount: 200 }); - +export default async function ResearchSection({ research }: { research: ResearchSearchResult }) { return (
diff --git a/app/[locale]/search/fetchContent.ts b/app/[locale]/search/fetchContent.ts new file mode 100644 index 00000000..8a3b8751 --- /dev/null +++ b/app/[locale]/search/fetchContent.ts @@ -0,0 +1,116 @@ +import { + searchAbout, + searchNotice, + searchNews, + searchMember, + searchResearch, + searchAdmissions, + searchAcademics, +} from '@/apis/search'; +import { getSeminarPosts } from '@/apis/seminar'; + +import { + AboutSearchResult, + NoticeSearchResult, + NewsSearchResult, + MemberSearchResult, + ResearchSearchResult, + AdmissionsSearchResult, + AcademicsSearchResult, +} from '@/types/search'; +import { SeminarList } from '@/types/seminar'; + +import { TreeNode } from './helper/SearchSubNavbar'; + +type SectionContent = [ + about?: AboutSearchResult, + notice?: NoticeSearchResult, + news?: NewsSearchResult, + seminar?: SeminarList, + member?: MemberSearchResult, + research?: ResearchSearchResult, + admission?: AdmissionsSearchResult, + academics?: AcademicsSearchResult, +]; + +// TOOD: 리팩터링 +export default async function fetchContent(keyword: string, tag?: string[]) { + const noTag = tag === undefined || tag.length === 0; + + // fetch + const sectionContent: SectionContent = await Promise.all([ + isSectionVisible('소개', tag) ? searchAbout({ keyword, number: 3, amount: 200 }) : undefined, + isSectionVisible('소식', tag) ? searchNotice({ keyword, number: 3, amount: 200 }) : undefined, + isSectionVisible('소식', tag) ? searchNews({ keyword, number: 3, amount: 200 }) : undefined, + isSectionVisible('소식', tag) ? getSeminarPosts({ keyword, pageNum: '1' }) : undefined, + isSectionVisible('구성원', tag) + ? searchMember({ keyword, number: 10, amount: 200 }) + : undefined, + isSectionVisible('연구', tag) ? searchResearch({ keyword, number: 3, amount: 200 }) : undefined, + isSectionVisible('입학', tag) + ? searchAdmissions({ keyword, number: 3, amount: 200 }) + : undefined, + isSectionVisible('학사 및 교과', tag) + ? searchAcademics({ keyword, number: 3, amount: 200 }) + : undefined, + ]); + + // 전체 개수 계산 + const total = sectionContent.reduce((prev, cur) => prev + (cur?.total ?? 0), 0); + + // 서브네비 구성 + const node: TreeNode[] = []; + node.push({ + name: `전체`, + size: tag === undefined || tag.length === 0 ? total : undefined, + bold: noTag, + }); + node.push({ + name: `소개`, + size: sectionContent[0]?.total, + bold: !noTag && sectionContent[0] !== undefined, + }); + + const noticeTotal = sectionContent[1]?.total; + const newsTotal = sectionContent[2]?.total; + const seminarTotal = sectionContent[3]?.total; + const sectionTotal = noticeTotal && (noticeTotal ?? 0) + (newsTotal ?? 0) + (seminarTotal ?? 0); + node.push({ + name: `소식`, + size: sectionTotal, + children: [ + { name: `공지사항`, size: noticeTotal }, + { name: `새 소식`, size: newsTotal }, + { name: `세미나`, size: seminarTotal }, + ], + bold: !noTag && sectionContent[1] !== undefined, + }); + + node.push({ + name: `구성원`, + size: sectionContent[4]?.total, + bold: !noTag && sectionContent[4] !== undefined, + }); + node.push({ + name: `연구`, + size: sectionContent[5]?.total, + bold: !noTag && sectionContent[5] !== undefined, + }); + node.push({ + name: `입학`, + size: sectionContent[6]?.total, + bold: !noTag && sectionContent[6] !== undefined, + }); + node.push({ + name: `학사 및 교과`, + size: sectionContent[7]?.total, + bold: !noTag && sectionContent[7] !== undefined, + }); + + return { sectionContent, node, total }; +} + +const isSectionVisible = ( + sectionName: '소개' | '소식' | '구성원' | '연구' | '입학' | '학사 및 교과', + tagList?: string[], +) => tagList === undefined || tagList.length === 0 || tagList.includes(sectionName); diff --git a/app/[locale]/search/helper/BasicRow.tsx b/app/[locale]/search/helper/BasicRow.tsx index e4b572d3..2dd0449a 100644 --- a/app/[locale]/search/helper/BasicRow.tsx +++ b/app/[locale]/search/helper/BasicRow.tsx @@ -2,6 +2,7 @@ import { Link } from '@/navigation'; import RangeBolded from '@/components/common/RangeBolded'; +import { getPath } from '@/utils/page'; import { SegmentNode } from '@/utils/segmentNode'; type BasicRowProps = { @@ -15,10 +16,15 @@ type BasicRowProps = { export default function BasicRow({ href, title, node, ...description }: BasicRowProps) { return ( - -

{title}

+
+ + {title} + -

{`${node.parent?.name} > ${node?.name}`}

- + {`${node.parent?.name} > ${node?.name}`} +
); } diff --git a/app/[locale]/search/helper/CircleTitle.tsx b/app/[locale]/search/helper/CircleTitle.tsx index 8ab9e550..58d7dd41 100644 --- a/app/[locale]/search/helper/CircleTitle.tsx +++ b/app/[locale]/search/helper/CircleTitle.tsx @@ -2,7 +2,7 @@ export default function CircleTitle({ title, size }: { title: string; size?: num // TODO: 번역 적용 // const t = useTranslations(); return ( -
+

{title} diff --git a/app/[locale]/search/helper/NavbarButton.tsx b/app/[locale]/search/helper/NavbarButton.tsx new file mode 100644 index 00000000..3368728c --- /dev/null +++ b/app/[locale]/search/helper/NavbarButton.tsx @@ -0,0 +1,28 @@ +'use client'; + +import { CSSProperties } from 'react'; + +import { TreeNode } from './SearchSubNavbar'; + +export default function NavbarButton({ node, style }: { node: TreeNode; style: CSSProperties }) { + return ( + + ); +} + +const scrollToSection = (id: string) => { + const target = document.getElementById(id.replace(' ', '_')); + if (target === null) return; + + const pos = target.getBoundingClientRect(); + window.scrollTo({ top: pos.top + window.scrollY - 100, behavior: 'smooth' }); +}; diff --git a/app/[locale]/search/helper/NewsRow.tsx b/app/[locale]/search/helper/NewsRow.tsx new file mode 100644 index 00000000..64131c50 --- /dev/null +++ b/app/[locale]/search/helper/NewsRow.tsx @@ -0,0 +1,67 @@ +import { Link } from '@/navigation'; + +import ImageWithFallback from '@/components/common/ImageWithFallback'; + +export interface NewsRowProps { + href: string; + title: string; + date: Date; + imageURL: string | null; + + description: { + partialDescription: string; + boldStartIndex: number; + boldEndIndex: number; + }; +} + +export default function NewsRow({ + href, + title, + description: { partialDescription, boldEndIndex, boldStartIndex }, + date, + imageURL, +}: NewsRowProps) { + const dateStr = date.toLocaleDateString('ko', { + year: 'numeric', + month: 'long', + day: 'numeric', + weekday: 'long', + }); + + return ( +
+ + + +
+
+ +

{title}

+ + + +

+ {partialDescription.slice(0, boldStartIndex)} + + {partialDescription.slice(boldStartIndex, boldEndIndex)} + + {partialDescription.slice(boldEndIndex)} +

+ +
+ + +
+
+ ); +} diff --git a/app/[locale]/search/helper/NoSearchResultError.tsx b/app/[locale]/search/helper/NoSearchResultError.tsx new file mode 100644 index 00000000..7d19b8da --- /dev/null +++ b/app/[locale]/search/helper/NoSearchResultError.tsx @@ -0,0 +1,10 @@ +import MagnificentGlass from '@/public/image/search/magnificent_glass.svg'; + +export default function NoSearchResultError() { + return ( +
+

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

+ +
+ ); +} diff --git a/app/[locale]/search/helper/NoticeRow.tsx b/app/[locale]/search/helper/NoticeRow.tsx index 32c22c7e..06712c05 100644 --- a/app/[locale]/search/helper/NoticeRow.tsx +++ b/app/[locale]/search/helper/NoticeRow.tsx @@ -18,11 +18,12 @@ const noticePath = getPath(notice); export default function NoticeRow({ id, title, dateStr, ...description }: NoticeRowProps) { const date = new Date(dateStr); + return (

{title}

-