Skip to content
Merged
44 changes: 37 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"@storybook/addon-docs": "10.3.6",
"@storybook/addon-vitest": "10.3.6",
"@storybook/react-vite": "10.3.6",
"@tanstack/react-query-devtools": "^5.101.1",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "repo react-query version:"
jq -r '.dependencies["`@tanstack/react-query`"]' package.json

echo "devtools@5.101.1 peer range for react-query:"
curl -s https://registry.npmjs.org/@tanstack%2Freact-query-devtools/5.101.1 \
  | jq -r '.peerDependencies["`@tanstack/react-query`"]'

Repository: alter-app/alter-client

Length of output: 249


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "package.json relevant lines:"
sed -n '1,140p' package.json | nl -ba | sed -n '1,140p'

echo
echo "tanstack-related dependencies:"
jq -r '.dependencies, .devDependencies | to_entries[] | select(.key|test("^`@tanstack/`")) | "\(.key)=\(.value)"' package.json

Repository: alter-app/alter-client

Length of output: 229


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "package.json relevant lines:"
sed -n '1,140p' package.json | nl -ba | sed -n '1,140p'

echo
echo "tanstack-related dependencies:"
jq -r '.dependencies, .devDependencies | to_entries[] | select(.key|test("^`@tanstack/`")) | "\(.key)=\(.value)"' package.json

Repository: alter-app/alter-client

Length of output: 229


@tanstack/react-query와 Devtools 버전을 맞춰주세요. @tanstack/react-query-devtools@5.101.1@tanstack/react-query@^5.101.1을 요구하므로, 현재 ^5.90.21과 peer 조건이 맞지 않습니다. react-query^5.101.1로 올리거나 Devtools를 같은 릴리스 라인으로 내리세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package.json` at line 48, `@tanstack/react-query-devtools` and
`@tanstack/react-query` are on mismatched release lines, causing the peer
dependency conflict. Update the dependency entry for `@tanstack/react-query` in
package.json to match the devtools version used by
`@tanstack/react-query-devtools`, or downgrade the devtools package so both stay
on the same compatible 5.x version.

"@types/node": "^24.10.1",
"@types/react": "^18.3.27",
"@types/react-dom": "^18.3.7",
Expand Down
11 changes: 10 additions & 1 deletion src/app/providers/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useEffect, type ReactNode } from 'react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import { initKakaoSDK, initAppleSDK } from '@/shared/lib/socialLogin'

const queryClient = new QueryClient()
Expand Down Expand Up @@ -55,6 +56,14 @@ export function AppProviders({ children }: AppProvidersProps) {
}, [])

return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
<QueryClientProvider client={queryClient}>
{children}
{import.meta.env.DEV ? (
<ReactQueryDevtools
initialIsOpen={false}
buttonPosition="bottom-right"
/>
) : null}
</QueryClientProvider>
)
}
19 changes: 16 additions & 3 deletions src/features/manager/api/substitute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@ import type {

export async function approveSubstituteRequest(
requestId: number,
body: { approvalComment: string }
body: { approvalComment?: string } = {}
): Promise<void> {
const payload =
body.approvalComment != null && body.approvalComment.trim() !== ''
? { approvalComment: body.approvalComment.trim() }
: {}
await axiosInstance.post(
`/manager/substitute-requests/${requestId}/approve`,
body
payload
)
}

Expand All @@ -27,6 +31,15 @@ export async function rejectSubstituteRequest(
export async function fetchSubstituteRequests(
params: SubstituteRequestsQueryParams
): Promise<SubstituteListApiResponse> {
const status =
params.status == null
? undefined
: Array.isArray(params.status)
? params.status.length > 0
? params.status
: undefined
: params.status

const response = await axiosInstance.get<SubstituteListApiResponse>(
'/manager/substitute-requests',
{
Expand All @@ -35,7 +48,7 @@ export async function fetchSubstituteRequests(
...(params.workspaceId !== undefined && {
workspaceId: params.workspaceId,
}),
...(params.status && { status: params.status }),
...(status != null && { status }),
...(params.cursor !== undefined && { cursor: params.cursor }),
},
}
Expand Down
Comment thread
dohy-eon marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import { useMemo } from 'react'
import { useInfiniteQuery } from '@tanstack/react-query'
import { fetchSubstituteRequests } from '@/features/manager/api/substitute'
import { resolveManagerApiStatuses } from '@/features/manager/substitute'
import { adaptSubstituteRequestDto } from '@/features/manager/home/types/substitute'
import type { SubstituteListStatusFilter } from '@/shared/types/substituteListFilters'
import { queryKeys } from '@/shared/lib/queryKeys'

export function useSubstituteRequestsViewModel(
workspaceId: number | null,
params?: { status?: string },
params?: { statusFilter?: SubstituteListStatusFilter },
pageSize = 10
) {
const statusFilter = params?.statusFilter ?? 'all'
const apiStatuses = resolveManagerApiStatuses(statusFilter)

const {
data,
fetchNextPage,
Expand All @@ -19,14 +24,14 @@ export function useSubstituteRequestsViewModel(
} = useInfiniteQuery({
queryKey: queryKeys.substitute.list({
workspaceId: workspaceId ?? undefined,
status: params?.status,
statusFilter,
pageSize,
}),
queryFn: ({ pageParam }) =>
fetchSubstituteRequests({
pageSize,
workspaceId: workspaceId ?? undefined,
status: params?.status,
status: apiStatuses.length > 0 ? apiStatuses : undefined,
cursor: pageParam as string | undefined,
}),
initialPageParam: undefined as string | undefined,
Expand Down
2 changes: 1 addition & 1 deletion src/features/manager/home/types/substitute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export type SubstituteListApiResponse = CommonApiResponse<{
// ---- Query Params ----
export interface SubstituteRequestsQueryParams {
workspaceId?: number
status?: string
status?: string | string[]
cursor?: string
pageSize: number
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ import {
approveSubstituteRequest,
rejectSubstituteRequest,
} from '@/features/manager/api/substitute'
import type { ManagerSubstituteListFilters } from '@/features/manager/substitute'
import type { SubstituteListStatusFilter } from '@/shared/types/substituteListFilters'
import type { SubstituteRequestItem } from '@/shared/types/substituteRequest'
import type { SubstituteActionType } from '@/pages/manager/substitute-request/components/ManagerSubstituteActionModal'
import { SubstituteApiStatus } from '@/shared/types/substituteStatus'
import { queryKeys } from '@/shared/lib/queryKeys'

const SUBSTITUTE_ACTION_ERROR_MESSAGES: Record<string, string> = {
B001: '이미 처리되었거나 승인/거절할 수 없는 상태입니다.',
Expand All @@ -30,8 +31,28 @@ function getSubstituteActionErrorMessage(error: unknown): string {

type ActionTarget = { id: number; type: SubstituteActionType }

export type ManagerSubstituteSectionKey = 'pending' | 'accepted' | 'cancelled'

export type ManagerSubstituteSection = {
key: ManagerSubstituteSectionKey
title: string
items: SubstituteRequestItem[]
}

const EMPTY_GROUPS = { pending: [], accepted: [], cancelled: [] }

const SECTION_TITLE: Record<ManagerSubstituteSectionKey, string> = {
pending: '요청됨',
accepted: '수락됨',
cancelled: '취소됨',
}

const SECTION_ORDER: ManagerSubstituteSectionKey[] = [
'pending',
'accepted',
'cancelled',
]

// ACCEPTED = 워커가 수락해 사장 승인 대기 중 → 요청됨
// APPROVED = 사장이 승인 → 수락됨
// REJECTED_BY_APPROVER = 사장이 거절 → 취소됨
Expand All @@ -53,24 +74,49 @@ function groupByStatus(requests: SubstituteRequestItem[]) {
return { pending, accepted, cancelled }
}

export function useManagerSubstituteRequestViewModel() {
function buildSections(
groups: ReturnType<typeof groupByStatus>,
statusFilter: SubstituteListStatusFilter
): ManagerSubstituteSection[] {
const allSections = SECTION_ORDER.map(key => ({
key,
title: SECTION_TITLE[key],
items: groups[key],
}))

if (statusFilter !== 'all') {
return allSections.filter(
section => section.key === statusFilter && section.items.length > 0
)
}

return allSections.filter(section => section.items.length > 0)
}

export function useManagerSubstituteRequestViewModel(
filters: ManagerSubstituteListFilters = { statusFilter: 'all' }
) {
const queryClient = useQueryClient()
const { activeWorkspaceId } = useManagedWorkspacesQuery()
const { requests, isLoading, isError } =
useSubstituteRequestsViewModel(activeWorkspaceId)
const { statusFilter } = filters
const { requests, isLoading, isError } = useSubstituteRequestsViewModel(
activeWorkspaceId,
{ statusFilter }
)
const [actionTarget, setActionTarget] = useState<ActionTarget | null>(null)
const [actionError, setActionError] = useState<string | null>(null)

const invalidate = () =>
queryClient.invalidateQueries({
queryKey: queryKeys.substitute.list({
workspaceId: activeWorkspaceId ?? undefined,
}),
queryKey: ['substitute', 'list'],
})

const approveMutation = useMutation({
mutationFn: ({ id, comment }: { id: number; comment: string }) =>
approveSubstituteRequest(id, { approvalComment: comment }),
approveSubstituteRequest(
id,
comment !== '' ? { approvalComment: comment } : {}
),
onSuccess: async () => {
await invalidate()
setActionTarget(null)
Expand All @@ -94,10 +140,10 @@ export function useManagerSubstituteRequestViewModel() {
},
})

const { pending, accepted, cancelled } = useMemo(
() => groupByStatus(requests),
[requests]
)
const sections = useMemo(() => {
const groups = groupByStatus(requests)
return buildSections(groups, statusFilter)
}, [requests, statusFilter])

const handleModalSubmit = (comment: string) => {
if (actionTarget === null) return
Expand All @@ -117,10 +163,8 @@ export function useManagerSubstituteRequestViewModel() {
return {
isLoading,
isError,
isEmpty: requests.length === 0,
pending,
accepted,
cancelled,
isEmpty: sections.length === 0,
sections,
actionsDisabled: approveMutation.isPending || rejectMutation.isPending,
actionModal: {
open: actionTarget !== null,
Expand Down
9 changes: 9 additions & 0 deletions src/features/manager/substitute/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export {
MANAGER_FILTER_TO_API_STATUS,
resolveManagerApiStatuses,
} from '@/features/manager/substitute/lib/managerSubstituteListFilters'
export type { ManagerSubstituteListFilters } from '@/features/manager/substitute/lib/managerSubstituteListFilters'
export {
useManagerSubstituteRequestViewModel,
type ManagerSubstituteSection,
} from '@/features/manager/substitute/hooks/useManagerSubstituteRequestViewModel'
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { SubstituteListStatusFilter } from '@/shared/types/substituteListFilters'
import { SubstituteApiStatus } from '@/shared/types/substituteStatus'

export type ManagerSubstituteListFilters = {
statusFilter: SubstituteListStatusFilter
}

/** 매니저 UI 필터 → API status (G5 기준) */
export const MANAGER_FILTER_TO_API_STATUS: Record<
SubstituteListStatusFilter,
SubstituteApiStatus[]
> = {
all: [],
pending: [SubstituteApiStatus.ACCEPTED],
accepted: [SubstituteApiStatus.APPROVED],
cancelled: [SubstituteApiStatus.REJECTED_BY_APPROVER],
}

export function resolveManagerApiStatuses(
filter: SubstituteListStatusFilter
): SubstituteApiStatus[] {
return MANAGER_FILTER_TO_API_STATUS[filter]
}
Loading
Loading