Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

공지사항 리스트 뷰 및 검색 페이지 추가 #126

Merged
merged 6 commits into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/service/.eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ module.exports = {
},
],
'@typescript-eslint/naming-convention': 'off',
'@@typescript-eslint/lines-between-class-members': 'off',
'@typescript-eslint/no-throw-literal': 'off',
},
settings: {
'import/resolver': {
Expand Down
12 changes: 12 additions & 0 deletions apps/service/src/app/(service)/(static)/announcement/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Header } from '@/widgets/menu'

export default function AnnouncementLayout({
children,
}: React.PropsWithChildren) {
return (
<>
<Header title="공지사항" />
{children}
</>
)
}
57 changes: 57 additions & 0 deletions apps/service/src/app/(service)/(static)/announcement/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Metadata } from 'next'
import {
AnnouncementEmptyItem,
AnnouncementList,
AnnouncementSearchInput,
AnnouncementTable,
PageController,
} from '@/widgets/announcement-list'
import {
getAnnouncementList,
getPinnedAnnouncementList,
SearchParams,
} from '@/entities/announcement'
import { Container } from '@/shared/ui'

export const metadata: Metadata = {
title: '공지사항',
}

export interface AnnouncementPageProps {
searchParams: SearchParams
}

export default async function AnnouncementPage({
searchParams,
}: AnnouncementPageProps) {
const [{ data }, { data: pinnedList }] = await Promise.all([
getAnnouncementList(searchParams),
getPinnedAnnouncementList(),
])

const totalLength = data.totalElements + pinnedList.length

return (
<Container className="p-5">
<div className="mb-6 flex items-center justify-end">
<AnnouncementSearchInput />
</div>
{totalLength > 0 && (
<p className="mb-4 mt-8 break-keep px-2">
{totalLength}개의 공지가 있어요
</p>
)}
<AnnouncementTable>
<AnnouncementEmptyItem render={totalLength <= 0} />
<AnnouncementList contents={pinnedList} pinned />
<AnnouncementList contents={data.content} />
</AnnouncementTable>
<PageController
current={data.number}
first={data.first}
last={data.last}
baseURL="/announcement"
/>
</Container>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Metadata } from 'next'
import {
SEARCH_TYPE,
searchAnnouncement,
SearchType,
} from '@/entities/announcement'
import { Container } from '@/shared/ui'
import {
AnnouncementSearchInput,
AnnouncementTable,
AnnouncementList,
PageController,
AnnouncementEmptyItem,
} from '@/widgets/announcement-list'
import { AnnouncementPageProps } from '../page'

export const metadata: Metadata = {
title: '공지사항 검색',
}

export default async function AnnouncementSearchPage({
searchParams,
}: AnnouncementPageProps) {
const { query = '', type = SEARCH_TYPE.TITLE } = searchParams
const validQuery = Array.isArray(query) ? query[query.length - 1] : query
const validType = (
Array.isArray(type) ? type[type.length - 1] : type
) as SearchType

const { data } = await searchAnnouncement({
query: validQuery,
type: validType,
})

return (
<Container className="p-5">
<div className="mb-6 flex items-center justify-end">
<AnnouncementSearchInput
defaultValue={validQuery}
currentType={validType}
/>
</div>
{data.totalElements > 0 && (
<p className="mb-4 mt-8 break-keep px-2">
{data.totalElements}개의 검색결과가 있어요
</p>
)}
<AnnouncementTable>
<AnnouncementEmptyItem
comment="검색결과가 없어요"
render={data.totalElements <= 0}
/>
<AnnouncementList contents={data.content} />
</AnnouncementTable>
<PageController
current={data.number}
first={data.first}
last={data.last}
baseURL={`/announcement/search?query=${validQuery}&type=${validType}`}
/>
</Container>
)
}
27 changes: 27 additions & 0 deletions apps/service/src/entities/announcement/api/createAnnouncement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export interface CreateAnnouncementBody {
title: string
content: string
postCategory: string
isPinned: false
}

export interface CreateAnnouncement {
(body: CreateAnnouncementBody): Promise<void>
}

export const createAnnouncement: CreateAnnouncement = async (body) => {
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/announcement`,
{
body: JSON.stringify(body),
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
},
)

if (!response.ok) {
throw new Error('공지사항을 생성하는데 실패했어요.')
}
}
14 changes: 14 additions & 0 deletions apps/service/src/entities/announcement/api/deleteAnnouncement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export interface DeleteAnnouncement {
(id: number): Promise<void>
}

export const deleteAnnouncement: DeleteAnnouncement = async (id) => {
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/announcement/${id}`,
{ method: 'DELETE' },
)

if (!response.ok) {
throw new Error('공지사항을 삭제하는데 실패했어요.')
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { PostDetail } from '../model'

export interface GetAnnouncementDetailResponse {
status: string
message: string
data: PostDetail
}

export interface GetAnnouncementDetail {
(id: number): Promise<GetAnnouncementDetailResponse>
}

export const getAnnouncementDetail: GetAnnouncementDetail = async (id) => {
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/announcement/${id}`,
{ method: 'GET' },
)

const data: GetAnnouncementDetailResponse = await response.json()

if (!response.ok) {
throw new Error('공지사항을 가져오는데 실패했어요.')
}

return data
}
69 changes: 69 additions & 0 deletions apps/service/src/entities/announcement/api/getAnnouncementList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import type { PostItem } from '../model'

export interface PaginationResponse<T> {
totalElements: number
totalPages: number
first: boolean
last: boolean
size: number
content: T[]
number: number
sort: {
empty: boolean
unsorted: boolean
sorted: boolean
}
numberOfElements: number
pageable: {
pageNumber: number
pageSize: number
sort: {
empty: boolean
unsorted: boolean
sorted: boolean
}
offset: number
unpaged: boolean
paged: boolean
}
empty: boolean
}

export interface SearchParams {
[key: string]: string | string[] | undefined
}

export interface GetAnnouncementListResponse {
status: string
message: string
data: PaginationResponse<PostItem>
}

export interface GetAnnouncementList {
(params: SearchParams): Promise<GetAnnouncementListResponse>
}

export const getAnnouncementList: GetAnnouncementList = async (params) => {
const targetURL = new URL(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/announcement`,
)

Object.entries(params).forEach(([key, value]) => {
if (value) {
targetURL.searchParams.set(
key,
Array.isArray(value) ? value[value.length - 1] : value,
)
}
})

const response = await fetch(targetURL, { method: 'GET' })

const data: GetAnnouncementListResponse = await response.json()

if (!response.ok) {
throw new Error('공지사항 목록을 가져오는데 실패했어요.')
}

return data
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { PostItem } from '../model'

export interface GetPinnedAnnouncementListResponse {
status: string
message: string
data: PostItem[]
}

export interface GetPinnedAnnouncementList {
(): Promise<GetPinnedAnnouncementListResponse>
}

export const getPinnedAnnouncementList: GetPinnedAnnouncementList =
async () => {
const targetURL = new URL(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/announcement/pinned`,
)

const response = await fetch(targetURL, { method: 'GET' })

const data: GetPinnedAnnouncementListResponse = await response.json()

if (!response.ok) {
throw new Error('고정된 공지사항 목록을 가져오는데 실패했어요.')
}

return data
}
7 changes: 7 additions & 0 deletions apps/service/src/entities/announcement/api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export * from './getAnnouncementList'
export * from './getPinnedAnnouncementList'
export * from './getAnnouncementDetail'
export * from './createAnnouncement'
export * from './deleteAnnouncement'
export * from './modifyAnnouncement'
export * from './searchAnnouncement'
22 changes: 22 additions & 0 deletions apps/service/src/entities/announcement/api/modifyAnnouncement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { CreateAnnouncementBody } from './createAnnouncement'

export interface ModifyAnnouncement {
(id: number, body: CreateAnnouncementBody): Promise<void>
}

export const modifyAnnouncement: ModifyAnnouncement = async (id, body) => {
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/announcement/${id}`,
{
body: JSON.stringify(body),
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
},
)

if (!response.ok) {
throw new Error('공지사항을 수정하는데 실패했어요.')
}
}
40 changes: 40 additions & 0 deletions apps/service/src/entities/announcement/api/searchAnnouncement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { GetAnnouncementListResponse } from './getAnnouncementList'

export type SearchType = keyof typeof SEARCH_TYPE

export interface SearchAnnouncementParams {
type: SearchType
query: string
}

export interface SearchAnnouncement {
(params: SearchAnnouncementParams): Promise<GetAnnouncementListResponse>
}

export const SEARCH_TYPE = Object.freeze({
TITLE: '제목',
CONTENT: '내용',
TITLE_CONTENT: '제목 및 내용',
})

export const searchAnnouncement: SearchAnnouncement = async (params) => {
const targetURL = new URL(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/announcement/search`,
)

Object.entries(params).forEach(([key, value]) => {
if (value) {
targetURL.searchParams.set(key, value)
}
})

const response = await fetch(targetURL, { method: 'GET' })

const data: GetAnnouncementListResponse = await response.json()

if (!response.ok) {
throw new Error('공지사항 검색에 실패했어요.')
}

return data
}
2 changes: 2 additions & 0 deletions apps/service/src/entities/announcement/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './api'
export * from './model'
Loading
Loading