Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
5 changes: 5 additions & 0 deletions src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { NicknameEditPage } from '@/pages/my/profile/nickname'
import { PasswordEditPage } from '@/pages/my/profile/password'
import { SocialAccountPage } from '@/pages/my/profile/social'
import { WithdrawPage } from '@/pages/my/withdraw'
import { ScrappedPostingsPage } from '@/pages/my/scrapped'
import { ErrorPageRoute } from '@/pages/error'
import { MobileLayout } from '@/shared/ui/MobileLayout'
import { MobileLayoutWithDocbar } from '@/shared/ui/MobileLayoutWithDocbar'
Expand Down Expand Up @@ -133,6 +134,10 @@ export function App() {
element={<SocialAccountPage />}
/>
<Route path={ROUTES.MY.WITHDRAW} element={<WithdrawPage />} />
<Route
path={ROUTES.MY.SCRAPPED_POSTINGS}
element={<ScrappedPostingsPage />}
/>
<Route
path={ROUTES.MANAGER.WORKER_SCHEDULE}
element={<ManagerWorkerScheduleLegacyEntryRedirect />}
Expand Down
182 changes: 166 additions & 16 deletions src/features/job-lookup-map/api/posting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,20 @@ import axiosInstance from '@/shared/lib/axiosInstance'
import type { CommonApiResponse } from '@/shared/types/common'
import type {
ApplyPostingRequest,
PostingListResponse,
FavoritePostingItem,
FavoritePostingListResponse,
PostingDetailResponse,
PostingFilterOptions,
PostingListResponse,
} from '@/features/job-lookup-map/types/posting'

import type { PostingsListFilters } from '@/features/job-lookup-map/lib/postingFilters'

export type FetchPostingsParams = {
pageSize: number
cursor?: string
searchKeyword?: string
}
} & PostingsListFilters

function isCommonApiEnvelope(
value: unknown
Expand All @@ -23,6 +28,52 @@ function isCommonApiEnvelope(
)
}

function normalizePostingListResponse(value: unknown): PostingListResponse {
const payload = isCommonApiEnvelope(value) ? value.data : value
if (payload === null || typeof payload !== 'object') {
throw new Error('공고 목록을 불러오지 못했습니다.')
}

const record = payload as Record<string, unknown>
const data = Array.isArray(record.data)
? (record.data as PostingListResponse['data'])
: []
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
const pageRaw = record.page

if (pageRaw !== null && typeof pageRaw === 'object') {
const page = pageRaw as Record<string, unknown>
const cursor = page.cursor
return {
data,
page: {
cursor:
typeof cursor === 'string'
? cursor
: cursor == null
? null
: String(cursor),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
pageSize:
typeof page.pageSize === 'number' ? page.pageSize : data.length,
totalCount:
typeof page.totalCount === 'number' ? page.totalCount : data.length,
},
}
}

return {
data,
page: {
cursor: null,
pageSize: data.length,
totalCount: data.length,
},
}
}

function unwrapPostingListBody(body: unknown): PostingListResponse {
return normalizePostingListResponse(body)
}

function isPostingDetailResponse(
value: unknown
): value is PostingDetailResponse {
Expand Down Expand Up @@ -63,20 +114,34 @@ function unwrapPostingDetailBody(body: unknown): PostingDetailResponse {
export async function fetchPostings(
params: FetchPostingsParams
): Promise<PostingListResponse> {
const response = await axiosInstance.get<PostingListResponse>(
'/app/postings',
{
params: {
pageSize: params.pageSize,
...(params.cursor !== undefined &&
params.cursor !== '' && { cursor: params.cursor }),
...(params.searchKeyword?.trim() && {
searchKeyword: params.searchKeyword.trim(),
}),
},
}
)
return response.data
const {
pageSize,
cursor,
searchKeyword,
province,
district,
town,
minPayAmount,
maxPayAmount,
payAmountSort,
} = params

const response = await axiosInstance.get<unknown>('/app/postings', {
params: {
pageSize,
...(cursor !== undefined && cursor !== '' && { cursor }),
...(searchKeyword?.trim() && {
searchKeyword: searchKeyword.trim(),
}),
...(province && { province }),
...(district && { district }),
...(town && { town }),
...(minPayAmount != null && { minPayAmount }),
...(maxPayAmount != null && { maxPayAmount }),
...(payAmountSort != null && { payAmountSort }),
},
})
return unwrapPostingListBody(response.data)
}

export async function fetchPostingDetail(
Expand All @@ -98,3 +163,88 @@ export async function applyPosting(
body
)
}

function normalizeFavoritePostingListResponse(
value: unknown
): FavoritePostingListResponse {
const payload = isCommonApiEnvelope(value) ? value.data : value
if (payload === null || typeof payload !== 'object') {
throw new Error('스크랩 목록을 불러오지 못했습니다.')
}

const record = payload as Record<string, unknown>
const data = Array.isArray(record.data)
? (record.data as FavoritePostingItem[])
: []
const pageRaw = record.page

if (pageRaw !== null && typeof pageRaw === 'object') {
const page = pageRaw as Record<string, unknown>
const cursor = page.cursor
return {
data,
page: {
cursor:
typeof cursor === 'string'
? cursor
: cursor == null
? null
: String(cursor),
pageSize:
typeof page.pageSize === 'number' ? page.pageSize : data.length,
totalCount:
typeof page.totalCount === 'number' ? page.totalCount : data.length,
},
}
}

return {
data,
page: {
cursor: null,
pageSize: data.length,
totalCount: data.length,
},
}
}

/** GET /app/users/me/postings/favorites — 사용자 공고 스크랩 목록 조회 */
export async function fetchFavoritePostings(params: {
pageSize: number
cursor?: string
}): Promise<FavoritePostingListResponse> {
const { pageSize, cursor } = params

const response = await axiosInstance.get<unknown>(
'/app/users/me/postings/favorites',
{
params: {
pageSize,
...(cursor !== undefined && cursor !== '' && { cursor }),
},
}
)
return normalizeFavoritePostingListResponse(response.data)
}

/** POST /app/users/me/postings/favorites/{postingId} — 사용자 공고 스크랩 등록 */
export async function addFavoritePosting(postingId: number): Promise<void> {
await axiosInstance.post<CommonApiResponse<Record<string, never>>>(
`/app/users/me/postings/favorites/${postingId}`
)
}

/** DELETE /app/users/me/postings/favorites/{postingId} — 사용자 공고 스크랩 삭제 */
export async function removeFavoritePosting(postingId: number): Promise<void> {
await axiosInstance.delete<CommonApiResponse<Record<string, never>>>(
`/app/users/me/postings/favorites/${postingId}`
)
}

/** GET /app/postings/filter-options — 공고 목록 필터 옵션 조회 */
export async function fetchPostingFilterOptions(): Promise<PostingFilterOptions> {
const response = await axiosInstance.get<
CommonApiResponse<PostingFilterOptions>
>('/app/postings/filter-options')
return response.data.data
}
Loading
Loading