Skip to content
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
d639315
feat: Toast·SelectDropdown·Skeleton 공용 컴포넌트 추가
limtjdghks Jul 26, 2026
117eff2
feat: 사장님 구인구직 라우트 상수·path 헬퍼 추가
limtjdghks Jul 26, 2026
64b1b07
feat: 사장님 구인구직 feature 슬라이스 구현
limtjdghks Jul 26, 2026
ffe7656
feat: 사장님 구인구직 화면 6종 구현 및 라우트 등록
limtjdghks Jul 26, 2026
f519133
feat: Docbar 사장님 전용 내 공고·지원자 탭 추가
limtjdghks Jul 26, 2026
05091cf
refactor: 구인구직 인라인 아이콘 SVG 자산으로 분리
limtjdghks Jul 26, 2026
edc1af5
feat: Navbar 뒤로가기 숨김 옵션(showBack) 추가
limtjdghks Jul 26, 2026
ca32bc2
fix: 내 공고 목록 하단 CTA 제거 및 Navbar 작성 아이콘 추가
limtjdghks Jul 26, 2026
8689cc6
fix: 공고별 지원자 목록 경로 분리 및 Docbar 숨김
limtjdghks Jul 26, 2026
ae8ba1c
refactor: 시간 픽커 컴포넌트 shared 계층으로 이동
limtjdghks Jul 27, 2026
048df1c
feat: 근무일정 시간 입력을 시간 픽커로 교체
limtjdghks Jul 27, 2026
923f379
remove: 공고 업직종 키워드 기능 제거
limtjdghks Jul 27, 2026
0b37eb2
fix: 지원자 목록 더보기 버튼 제거 및 무한스크롤 적용
limtjdghks Jul 27, 2026
6af44dc
fix: 공고 수정 시 업장 변경 비활성화
limtjdghks Jul 27, 2026
582ea84
fix: 근무일정 모집 인원·시간 픽커 입력 버그 수정
limtjdghks Jul 27, 2026
b5217d0
fix: 공고 수정 폼 리마운트 key 위치 수정
limtjdghks Jul 27, 2026
357ee28
refactor: 요일·급여·상대시간 포맷터 shared 추출
limtjdghks Jul 27, 2026
9f5e8fb
refactor: ApplicationApiStatus shared 타입으로 이동
limtjdghks Jul 27, 2026
c84bcbd
refactor: 커서 페이징·시간 포맷 공용 유틸 추가
limtjdghks Jul 29, 2026
a38b286
feat: 매니저 공고·지원자 API 연동 및 목업 제거
limtjdghks Jul 29, 2026
bcf7ab3
fix: 매니저 홈 진행 중 공고가 항상 0건으로 표시되는 문제
limtjdghks Jul 29, 2026
b7f14d4
chore: 매니저 공고 슬라이스 불필요한 주석 제거
limtjdghks Jul 29, 2026
f7c8516
fix: 지원자 성별 누락 시 남성으로 표시되는 문제
limtjdghks Jul 29, 2026
281984a
feat: 매니저 공고 상태와 지원자 조회 보완
limtjdghks Aug 4, 2026
412014f
feat: 공고 일정 자동 마감과 서버 검증 오류 처리
limtjdghks Aug 4, 2026
84c06a9
fix: 공고 지원 오류와 재시도 처리 보완
limtjdghks Aug 4, 2026
65ea555
fix: 공고 등록 중 이전 서버 오류 적용 방지
limtjdghks Aug 4, 2026
cdf85d3
fix: 지원자 상세 정보 표시 형식 보완
limtjdghks Aug 5, 2026
a3d3847
feat: 공고 상세 제목 영역 UI 개선
limtjdghks Aug 5, 2026
7df2499
feat: 공고 근무일정 입력 UI 개선
limtjdghks Aug 5, 2026
4b12f6e
feat: 공고 상세 내용 입력 높이 자동 조절
limtjdghks Aug 5, 2026
e318fca
feat: 마감 공고 수정 제한
limtjdghks Aug 5, 2026
7c97a51
feat: 내 공고 상단 영역 고정
limtjdghks Aug 5, 2026
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
65 changes: 65 additions & 0 deletions src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ import { StoreRegisterRequestsPage } from '@/pages/store-register/requests'
import { StoreRegisterRequestDetailPage } from '@/pages/store-register/request-detail'
import { ManagerWorkerInvitePage } from '@/pages/manager/worker-invite'
import { WorkspaceImageEditPage } from '@/pages/manager/workspace-image-edit'
import { ManagerPostingListPage } from '@/pages/manager/posting-list'
import { ManagerPostingCreatePage } from '@/pages/manager/posting-create'
import { ManagerPostingDetailPage } from '@/pages/manager/posting-detail'
import { ManagerPostingEditPage } from '@/pages/manager/posting-edit'
import { ManagerApplicationListPage } from '@/pages/manager/application-list'
import { ManagerApplicationDetailPage } from '@/pages/manager/application-detail'
import { WorkspaceJoinPage } from '@/pages/user/workspace-join'
import { NotificationPage } from '@/pages/notification'
import { NotificationSettingsPage } from '@/pages/notification/settings'
Expand All @@ -44,6 +50,7 @@ import { ErrorPageRoute } from '@/pages/error'
import { MobileLayout } from '@/shared/ui/MobileLayout'
import { MobileLayoutWithDocbar } from '@/shared/ui/MobileLayoutWithDocbar'
import { HomeRouteGuard } from '@/shared/ui/common/HomeRouteGuard'
import { ToastViewport } from '@/shared/ui/common/ToastViewport'
import { ROUTES } from '@/shared/constants/routes'

const SignupPage = lazy(async () => {
Expand Down Expand Up @@ -161,6 +168,47 @@ export function App() {
path={ROUTES.MANAGER.WORKSPACE_IMAGES_EDIT_PATTERN}
element={<WorkspaceImageEditPage />}
/>
{/* 사장님 구인구직 — 정적 세그먼트가 :postingId보다 우선 매칭됩니다 */}
<Route
path={ROUTES.MANAGER.POSTING_NEW}
element={
<HomeRouteGuard expected="MANAGER">
<ManagerPostingCreatePage />
</HomeRouteGuard>
}
/>
<Route
path={ROUTES.MANAGER.POSTING_APPLICATIONS_BY_POSTING_PATTERN}
element={
<HomeRouteGuard expected="MANAGER">
<ManagerApplicationListPage />
</HomeRouteGuard>
}
/>
<Route
path={ROUTES.MANAGER.POSTING_APPLICATION_DETAIL_PATTERN}
element={
<HomeRouteGuard expected="MANAGER">
<ManagerApplicationDetailPage />
</HomeRouteGuard>
}
/>
<Route
path={ROUTES.MANAGER.POSTING_EDIT_PATTERN}
element={
<HomeRouteGuard expected="MANAGER">
<ManagerPostingEditPage />
</HomeRouteGuard>
}
/>
<Route
path={ROUTES.MANAGER.POSTING_DETAIL_PATTERN}
element={
<HomeRouteGuard expected="MANAGER">
<ManagerPostingDetailPage />
</HomeRouteGuard>
}
/>
</Route>

<Route element={<MobileRouteLayoutWithDocbar />}>
Expand Down Expand Up @@ -205,6 +253,22 @@ export function App() {
path={ROUTES.MANAGER.SOCIAL_CHAT}
element={<SocialChatPage />}
/>
<Route
path={ROUTES.MANAGER.POSTINGS}
element={
<HomeRouteGuard expected="MANAGER">
<ManagerPostingListPage />
</HomeRouteGuard>
}
/>
<Route
path={ROUTES.MANAGER.POSTING_APPLICATIONS}
element={
<HomeRouteGuard expected="MANAGER">
<ManagerApplicationListPage />
</HomeRouteGuard>
}
/>
<Route path={ROUTES.MY.ROOT} element={<MyPage />} />
<Route
path={ROUTES.USER.SUBSTITUTE_REQUEST_DETAIL_PATTERN}
Expand All @@ -227,6 +291,7 @@ export function App() {
<Route path="/" element={<Navigate to={ROUTES.AUTH.LOGIN} replace />} />
<Route path="*" element={<ErrorPageRoute />} />
</Routes>
<ToastViewport />
</BrowserRouter>
)
}
6 changes: 6 additions & 0 deletions src/assets/icons/doc/Applicant.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/assets/icons/posting/Alert.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/assets/icons/posting/Calendar.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/assets/icons/posting/CheckCircle.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/assets/icons/posting/ChevronRight.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/assets/icons/posting/Clock.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/assets/icons/posting/Close.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/assets/icons/posting/Lock.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/assets/icons/posting/Person.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/assets/icons/posting/Plus.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/assets/icons/posting/Write.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/features/job-lookup-map/hooks/useApplyPosting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export function useApplyPosting() {
const queryClient = useQueryClient()

return useMutation({
retry: false,
mutationFn: ({ postingId, body }: ApplyPostingVariables) =>
applyPosting(postingId, body),
onSuccess: (_, { postingId }) => {
Expand Down
31 changes: 31 additions & 0 deletions src/features/job-lookup-map/lib/applyPostingError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import axios from 'axios'

import { getAxiosErrorMessage } from '@/shared/lib/getAxiosErrorMessage'
import type { ErrorResponse } from '@/shared/types/common'

export interface ApplyPostingError {
message: string
retryable: boolean
}

export function resolveApplyPostingError(error: unknown): ApplyPostingError {
if (axios.isAxiosError(error)) {
const response = error.response?.data as ErrorResponse | undefined
const retryable =
error.response?.status === 429 || response?.code === 'E001'

if (retryable) {
return {
message:
response?.message ??
'요청이 너무 많습니다. 잠시 후 다시 시도해 주세요.',
retryable: true,
}
}
}

return {
message: getAxiosErrorMessage(error, '지원에 실패했습니다.'),
retryable: false,
}
}
48 changes: 48 additions & 0 deletions src/features/job-lookup-map/test/lib/applyPostingError.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { describe, expect, it } from 'vitest'

import { resolveApplyPostingError } from '../../lib/applyPostingError'

function axiosError(status: number, data: unknown) {
return {
isAxiosError: true,
message: `Request failed with status code ${status}`,
response: { status, data },
}
}

describe('공고 지원 오류', () => {
it.each([
'이미 지원한 공고입니다.',
'모집이 종료된 공고입니다.',
'삭제된 근무일정입니다.',
])('서버 메시지 %s를 표시한다', message => {
expect(
resolveApplyPostingError(axiosError(400, { code: 'B001', message }))
).toEqual({ message, retryable: false })
})

it('HTTP 429를 재시도 가능한 오류로 분류한다', () => {
expect(
resolveApplyPostingError(
axiosError(429, {
code: 'E001',
message: '요청이 너무 많습니다. 잠시 후 다시 시도해주세요.',
})
)
).toEqual({
message: '요청이 너무 많습니다. 잠시 후 다시 시도해주세요.',
retryable: true,
})
})

it('E001 코드를 재시도 가능한 오류로 분류한다', () => {
expect(
resolveApplyPostingError(
axiosError(400, {
code: 'E001',
message: '잠시 후 다시 시도해 주세요.',
})
).retryable
).toBe(true)
})
})
6 changes: 4 additions & 2 deletions src/features/manager/api/posting.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import axiosInstance from '@/shared/lib/axiosInstance'
import { unwrapCursorPage, type CursorPage } from '@/shared/lib/cursorPage'
import type {
PostingDto,
PostingListApiResponse,
ManagedPostingsQueryParams,
} from '@/features/manager/home/types/posting'

export async function fetchManagedPostings(
params: ManagedPostingsQueryParams
): Promise<PostingListApiResponse> {
): Promise<CursorPage<PostingDto>> {
const response = await axiosInstance.get<PostingListApiResponse>(
'/manager/postings',
{
Expand All @@ -20,5 +22,5 @@ export async function fetchManagedPostings(
},
}
)
return response.data
return unwrapCursorPage(response.data)
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,17 @@ export function useManagedPostingsViewModel(
cursor: pageParam as string | undefined,
}),
initialPageParam: undefined as string | undefined,
getNextPageParam: lastPage => lastPage.data?.page?.cursor ?? undefined,
getNextPageParam: lastPage => lastPage.page?.cursor ?? undefined,
enabled: workspaceId !== null,
})

const postings = useMemo(() => {
const all =
data?.pages.flatMap(
page => page.data?.data?.map(adaptPostingDto) ?? []
) ?? []
data?.pages.flatMap(page => page.data?.map(adaptPostingDto) ?? []) ?? []
return [...new Map(all.map(p => [p.id, p])).values()]
}, [data])

const totalCount = data?.pages[0]?.data?.page?.totalCount ?? 0
const totalCount = data?.pages[0]?.page?.totalCount ?? 0

return {
postings,
Expand Down
58 changes: 14 additions & 44 deletions src/features/manager/home/types/posting.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
import { PAYMENT_TYPE_LABEL as SHARED_PAYMENT_TYPE_LABEL } from '@/shared/constants/payment'
import { WORKING_DAYS, WORKING_DAY_LABEL } from '@/shared/constants/workingDays'
import { toTimeOfDay } from '@/shared/lib/toTimeOfDay'
import type { CursorPage } from '@/shared/lib/cursorPage'
import type { CommonApiResponse } from '@/shared/types/common'
import type { JobPostingItem } from '@/shared/ui/manager/OngoingPostingCard'

// ---- API DTOs ----
export interface PostingKeywordDto {
id: number
name: string
}

export interface PostingScheduleDto {
workingDays: string[]
startTime: string
endTime: string
positionsNeeded: number
position: number
position?: string
}

export interface PostingWorkspaceDto {
id: number
businessName: string
businessType?: string
businessTypeDetail?: string
}

export interface PostingDto {
Expand All @@ -26,21 +27,13 @@ export interface PostingDto {
payAmount: number
paymentType: string
createdAt: string
keywords: PostingKeywordDto[]
schedules: PostingScheduleDto[]
workspace: PostingWorkspaceDto
}

export interface PostingPageDto {
cursor: string | null
pageSize: number
totalCount: number
}

export type PostingListApiResponse = CommonApiResponse<{
page: PostingPageDto
data: PostingDto[]
}>
export type PostingListApiResponse =
| CursorPage<PostingDto>
| CommonApiResponse<CursorPage<PostingDto>>

// ---- Query Params ----
export interface ManagedPostingsQueryParams {
Expand All @@ -51,22 +44,9 @@ export interface ManagedPostingsQueryParams {
}

// ---- Mappers ----
const PAYMENT_TYPE_LABEL: Record<string, string> = {
HOURLY: '시급',
DAILY: '일급',
MONTHLY: '월급',
WEEKLY: '주급',
}
const PAYMENT_TYPE_LABEL: Record<string, string> = SHARED_PAYMENT_TYPE_LABEL

const WORKING_DAY_KO: Record<string, string> = {
MONDAY: '월',
TUESDAY: '화',
WEDNESDAY: '수',
THURSDAY: '목',
FRIDAY: '금',
SATURDAY: '토',
SUNDAY: '일',
}
const WORKING_DAY_KO: Record<string, string> = WORKING_DAY_LABEL

function formatWage(payAmount: number, paymentType: string): string {
const label = PAYMENT_TYPE_LABEL[paymentType] ?? paymentType
Expand All @@ -77,24 +57,14 @@ function formatWage(payAmount: number, paymentType: string): string {
function formatWorkHours(schedules: PostingScheduleDto[]): string {
if (schedules.length === 0) return '-'
const first = schedules[0]
const base = `${first.startTime} ~ ${first.endTime}`
const base = `${toTimeOfDay(first.startTime)} ~ ${toTimeOfDay(first.endTime)}`
return schedules.length > 1 ? `${base} 외 ${schedules.length - 1}개` : base
}

function formatWorkDays(schedules: PostingScheduleDto[]): string {
if (schedules.length === 0) return '-'
// 모든 스케줄의 요일을 합산 후 중복 제거 + 요일 순서 정렬
const DAY_ORDER = [
'MONDAY',
'TUESDAY',
'WEDNESDAY',
'THURSDAY',
'FRIDAY',
'SATURDAY',
'SUNDAY',
]
const daySet = new Set(schedules.flatMap(s => s.workingDays))
return DAY_ORDER.filter(d => daySet.has(d))
return WORKING_DAYS.filter(d => daySet.has(d))
.map(d => WORKING_DAY_KO[d] ?? d)
.join(', ')
}
Expand Down
Loading
Loading