Skip to content
Open
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
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
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,18 @@ import { useMemo } from 'react'
import { useInfiniteQuery } from '@tanstack/react-query'
import { fetchSubstituteRequests } from '@/features/manager/api/substitute'
import { adaptSubstituteRequestDto } from '@/features/manager/home/types/substitute'
import { resolveManagerApiStatuses } from '@/features/manager/substitute/lib/managerSubstituteListFilters'
import type { SubstituteListStatusFilter } from '@/features/user/substitute/lib/substituteListFilters'
Comment on lines +5 to +6

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

상태 필터 매핑을 feature 밖으로 올려주세요.

manager/home 훅이 manager/substituteuser/substitute 내부 lib에 직접 의존합니다. 공용 상태 필터 타입/매핑을 shared 또는 entities로 이동하면 feature 간 의존성을 끊을 수 있습니다.

As per path instructions, “src/features/** … entities, shared 레이어만 import하는지”를 확인해야 합니다.

🤖 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 `@src/features/manager/home/hooks/useSubstituteRequestsViewModel.ts` around
lines 5 - 6, `useSubstituteRequestsViewModel`가 `manager/substitute`와
`user/substitute`의 내부 lib에 직접 의존하고 있어 feature 간 결합이 생깁니다.
`resolveManagerApiStatuses`와 `SubstituteListStatusFilter`를 `shared` 또는
`entities` 레이어로 옮겨 공용 상태 필터 타입/매핑으로 재배치한 뒤, 이 훅에서는 그 공용 모듈만 import하도록 수정하세요.

Source: Path instructions

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/lib/managerSubstituteListFilters'
import type { SubstituteListStatusFilter } from '@/features/user/substitute/lib/substituteListFilters'
Comment on lines +10 to +11

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

필터 타입을 user feature에서 직접 가져오지 마세요.

manager/substitutefeatures/user/substitute 타입에 의존해서 feature 간 결합이 생깁니다. SubstituteListStatusFilter 같은 공용 필터 계약은 shared/entities로 옮기거나 manager 전용 타입으로 분리해주세요.

As per path instructions, “src/features/** … entities, shared 레이어만 import하는지”를 확인해야 합니다.

🤖 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
`@src/features/manager/substitute/hooks/useManagerSubstituteRequestViewModel.ts`
around lines 10 - 11, The hook is importing a filter type from another feature,
creating an unwanted cross-feature dependency. Update
useManagerSubstituteRequestViewModel to stop importing
SubstituteListStatusFilter from features/user/substitute and instead use a
shared or manager-owned contract; if needed, move the filter type to a
shared/entities location and adjust ManagerSubstituteListFilters and related
usages to reference that local/shared type.

Source: Path instructions

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
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { SubstituteListStatusFilter } from '@/features/user/substitute/lib/substituteListFilters'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

매니저 feature가 user feature 타입에 직접 의존하고 있습니다.

@/features/user/... 타입 import는 feature 레이어 경계를 깨뜨립니다. SubstituteListStatusFiltersrc/shared/**로 이동해 공용화하거나, 매니저 feature 내부 타입으로 분리해 의존 방향을 정리해주세요.

수정 예시(단기 완화안)
-import type { SubstituteListStatusFilter } from '`@/features/user/substitute/lib/substituteListFilters`'
 import { SubstituteApiStatus } from '`@/shared/types/substituteStatus`'
 
+type SubstituteListStatusFilter = 'all' | 'pending' | 'accepted' | 'cancelled'
+
 export type ManagerSubstituteListFilters = {
   statusFilter: SubstituteListStatusFilter
 }

As per path instructions, "src/features/**: entities, shared 레이어만 import하는지" 규칙을 기준으로 확인했습니다.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import type { SubstituteListStatusFilter } from '@/features/user/substitute/lib/substituteListFilters'
type SubstituteListStatusFilter = 'all' | 'pending' | 'accepted' | 'cancelled'
🤖 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 `@src/features/manager/substitute/lib/managerSubstituteListFilters.ts` at line
1, The manager substitute filter module is importing a type from the user
feature, which breaks the feature-layer dependency rule. Update
managerSubstituteListFilters to stop referencing SubstituteListStatusFilter from
the user feature by either moving that type into a shared location under
src/shared/** or defining an equivalent manager-local type within the manager
feature, and then adjust the import so manager code only depends on shared or
internal symbols.

Source: Path instructions

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