Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
ab2f531
feat: 업장 등록 신청 내역/상세 라우트 및 쿼리키 추가
ysw789 Jun 11, 2026
a9838ea
feat: 업장 등록 요청 API를 scope 기반으로 전환하고 취소/반려사유/댓글 연동 추가
ysw789 Jun 11, 2026
31d5dce
feat: 업장 등록 신청 내역/상세/반려사유 댓글 화면 추가
ysw789 Jun 11, 2026
cbbef5e
feat: 업장 등록 위저드에 대표자 성명/위임장 선택/입력 검증 보완
ysw789 Jun 11, 2026
882a4ab
feat: 마이페이지에 사장님 계정 전환(업장 등록 신청 내역) 진입점 추가
ysw789 Jun 11, 2026
dbb39fa
feat: 업장 등록 신청 플로우 공용 아이콘 모듈 및 파일 크기 포매팅 유틸 추가
ysw789 Jun 16, 2026
1370f0d
feat: 신청 내역 목록 화면 레이아웃 정렬 (카드 스타일, 스켈레톤 로딩, 에러/빈 상태, 고정 CTA)
ysw789 Jun 16, 2026
f11f393
feat: 신청 상세 화면 레이아웃 정렬 (상태별 정보 박스, 서류 썸네일 행, Spinner 로더)
ysw789 Jun 16, 2026
ec0b28f
feat: 반려 사유 + 댓글 스레드 UI 정렬 (경고 아이콘 사유 카드, 비대칭 댓글 버블, 하단 입력바, 255자 카운터)
ysw789 Jun 16, 2026
b0d888d
feat: 업장 등록 위저드 UI 정렬 (라벨 필드, 컴팩트 파일 업로더, 스텝 인디케이터, 고정 푸터)
ysw789 Jun 16, 2026
e00ceff
feat: 업장 등록 신청 카드 UI 세부 정렬 (텍스트 스타일, 배지 너비)
ysw789 Jun 16, 2026
c96dde0
feat: 마이페이지 메뉴에서 중복 항목 제거 (사장님 계정 전환)
ysw789 Jun 16, 2026
5c760c2
refactor: 반려 사유 관련 구성요소 제거 (Reason* 컴포넌트 및 훅)
ysw789 Jun 17, 2026
3147d96
fix: 업장 등록 신청 상세 페이지에서 승인 완료 상태 시 메시지 박스 및 버튼 제거
ysw789 Jun 17, 2026
d1b627a
feat: 업장 등록 신청 댓글 스레드 UI 구현
ysw789 Jun 17, 2026
cfbd41b
feat: 댓글 스레드 ViewModel 구현
ysw789 Jun 17, 2026
f4f3e82
feat: 댓글 스레드 API 추가 (생성, 조회, 파일 업로드)
ysw789 Jun 17, 2026
fe0206c
feat: 댓글 스레드 데이터 타입 및 쿼리키 정의
ysw789 Jun 17, 2026
f4a9a34
feat: 페이지에 댓글 스레드 통합 및 지원 파일 업데이트
ysw789 Jun 17, 2026
bb5ecb4
feat: 승인 완료 요청에서도 댓글 스레드 노출
ysw789 Jun 17, 2026
b0242c7
fix: BRN 검증 로직 강화
ysw789 Jun 17, 2026
8372da1
fix: 프로토타입 키 우회 방지
ysw789 Jun 17, 2026
9f0ff4b
fix: 업장 등록 requestId 유효성 검사 및 a11y 개선
ysw789 Jun 17, 2026
af4ee05
fix: 승인 완료 상태에서 로딩/에러 피드백 노출 개선
ysw789 Jun 17, 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
10 changes: 10 additions & 0 deletions src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import { AppliedStoresPage } from '@/pages/user/applied-stores'
import { SubstituteRequestPage } from '@/pages/user/substitute-request'
import { ManagerSubstituteRequestPage } from '@/pages/manager/substitute-request'
import { StoreRegisterPage } from '@/pages/manager/store-register'
import { StoreRegisterRequestsPage } from '@/pages/store-register/requests'
import { StoreRegisterRequestDetailPage } from '@/pages/store-register/request-detail'
import { ManagerWorkerInvitePage } from '@/pages/manager/worker-invite'
import { WorkerListPage } from '@/pages/manager/worker-list'
import { WorkspaceJoinPage } from '@/pages/user/workspace-join'
Expand Down Expand Up @@ -143,6 +145,14 @@ export function App() {
path={ROUTES.MANAGER.STORE_REGISTER}
element={<StoreRegisterPage />}
/>
<Route
path={ROUTES.STORE_REGISTER.REQUESTS}
element={<StoreRegisterRequestsPage />}
/>
<Route
path={ROUTES.STORE_REGISTER.REQUEST_DETAIL_PATTERN}
element={<StoreRegisterRequestDetailPage />}
/>
<Route
path={ROUTES.MANAGER.WORKER_INVITE}
element={<ManagerWorkerInvitePage />}
Expand Down
17 changes: 16 additions & 1 deletion src/features/store-register/api/workspaceFileUpload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,26 @@ const KIND_TO_TARGET: Record<
/** 업장 등록 신청용 파일 업로드 (targetType·bucket 고정 조합). */
export async function uploadWorkspaceRegistrationFile(
file: File,
kind: WorkspaceRegistrationAttachmentKind
kind: WorkspaceRegistrationAttachmentKind,
scope?: 'MANAGER' | 'USER' | null
): Promise<string> {
return uploadAppFile({
file,
targetType: KIND_TO_TARGET[kind],
bucketType: WORKSPACE_REGISTRATION_BUCKET,
scope,
})
}

/** 신청 댓글 첨부 파일 업로드 (targetType=WORKSPACE_REQUEST_COMMENT). */
export async function uploadWorkspaceRequestCommentFile(
file: File,
scope?: 'MANAGER' | 'USER' | null
): Promise<string> {
return uploadAppFile({
file,
targetType: 'WORKSPACE_REQUEST_COMMENT',
bucketType: WORKSPACE_REGISTRATION_BUCKET,
scope,
})
}
64 changes: 55 additions & 9 deletions src/features/store-register/api/workspaceRequests.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,85 @@
import axiosInstance from '@/shared/lib/axiosInstance'
import { getAuthApiBasePath } from '@/shared/lib/authApiPath'
import type { CommonApiResponse } from '@/shared/types/common'
import type {
CreateWorkspaceRequestCommentBody,
WorkspaceRequestCommentDto,
WorkspaceRequestCommentsApiResponse,
WorkspaceRequestDetailApiResponse,
WorkspaceRequestDetailDto,
WorkspaceRegistrationCreateBody,
WorkspaceRequestsListApiResponse,
WorkspaceRequestListItemDto,
} from '@/features/store-register/types/workspaceRequests'

/** GET /app/workspace-requests */
export async function fetchWorkspaceRequests(): Promise<
WorkspaceRequestListItemDto[]
> {
type AuthScope = 'MANAGER' | 'USER' | null | undefined

/** `/app/workspace-requests` | `/manager/workspace-requests` */
function requestsBasePath(scope: AuthScope): string {
return `/${getAuthApiBasePath(scope)}/workspace-requests`
}

/** GET /{scope}/workspace-requests */
export async function fetchWorkspaceRequests(
scope: AuthScope
): Promise<WorkspaceRequestListItemDto[]> {
const response = await axiosInstance.get<WorkspaceRequestsListApiResponse>(
'/app/workspace-requests'
requestsBasePath(scope)
)
return response.data.data
}

/** GET /app/workspace-requests/{workspaceRequestId} */
/** GET /{scope}/workspace-requests/{workspaceRequestId} */
export async function fetchWorkspaceRequestDetail(
scope: AuthScope,
workspaceRequestId: number
): Promise<WorkspaceRequestDetailDto> {
const response = await axiosInstance.get<WorkspaceRequestDetailApiResponse>(
`/app/workspace-requests/${workspaceRequestId}`
`${requestsBasePath(scope)}/${workspaceRequestId}`
)
return response.data.data
}

/** POST /app/workspace-requests */
/** POST /{scope}/workspace-requests */
export async function createWorkspaceRegistrationRequest(
scope: AuthScope,
body: WorkspaceRegistrationCreateBody
): Promise<void> {
await axiosInstance.post<CommonApiResponse<Record<string, never>>>(
'/app/workspace-requests',
requestsBasePath(scope),
body
)
}

/** POST /{scope}/workspace-requests/{id}/cancel — PENDING/REVOKED 에서만 성공 */
export async function cancelWorkspaceRequest(
scope: AuthScope,
workspaceRequestId: number
): Promise<void> {
await axiosInstance.post<CommonApiResponse<Record<string, never>>>(
`${requestsBasePath(scope)}/${workspaceRequestId}/cancel`
)
}

/** GET /{scope}/workspace-requests/{id}/comments */
export async function fetchWorkspaceRequestComments(
scope: AuthScope,
workspaceRequestId: number
): Promise<WorkspaceRequestCommentDto[]> {
const response = await axiosInstance.get<WorkspaceRequestCommentsApiResponse>(
`${requestsBasePath(scope)}/${workspaceRequestId}/comments`
)
return response.data.data
}

/** POST /{scope}/workspace-requests/{id}/comments */
export async function createWorkspaceRequestComment(
scope: AuthScope,
workspaceRequestId: number,
body: CreateWorkspaceRequestCommentBody
): Promise<void> {
await axiosInstance.post<CommonApiResponse<Record<string, never>>>(
`${requestsBasePath(scope)}/${workspaceRequestId}/comments`,
body
)
}
102 changes: 102 additions & 0 deletions src/features/store-register/hooks/useRequestCommentThreadViewModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { useCallback, useState } from 'react'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import useAuthStore from '@/shared/stores/useAuthStore'
import { useCertificateFilePick } from '@/shared/hooks/useCertificateFilePick'
import { queryKeys } from '@/shared/lib/queryKeys'
import { getAxiosErrorMessage } from '@/shared/lib/getAxiosErrorMessage'
import {
createWorkspaceRequestComment,
fetchWorkspaceRequestComments,
} from '@/features/store-register/api/workspaceRequests'
import { uploadWorkspaceRequestCommentFile } from '@/features/store-register/api/workspaceFileUpload'

export const COMMENT_MAX_LENGTH = 255

/** 첨부 파일 ≤ 5MB */
const ATTACHMENT_MAX_BYTES = 5 * 1024 * 1024

/** 신청 1건의 단일 댓글 스레드 + 작성 ViewModel */
export function useRequestCommentThreadViewModel(
requestId: number,
enabled: boolean
) {
const queryClient = useQueryClient()
const scope = useAuthStore(state => state.scope)
const attachment = useCertificateFilePick({ maxBytes: ATTACHMENT_MAX_BYTES })

const [comment, setComment] = useState('')
const [submitError, setSubmitError] = useState<string | null>(null)

const commentsKey = queryKeys.storeRegisterRequest.comments(scope, requestId)

const query = useQuery({
queryKey: commentsKey,
queryFn: () => fetchWorkspaceRequestComments(scope, requestId),
enabled: enabled && Number.isFinite(requestId),
})

const mutation = useMutation({
mutationFn: async () => {
const trimmed = comment.trim()
let fileIds: string[] | undefined
if (attachment.file) {
const fileId = await uploadWorkspaceRequestCommentFile(
attachment.file,
scope
)
fileIds = [fileId]
}
await createWorkspaceRequestComment(scope, requestId, {
comment: trimmed,
fileIds,
})
},
onSuccess: async () => {
setComment('')
attachment.clear()
await queryClient.invalidateQueries({ queryKey: commentsKey })
},
onError: (e: unknown) => {
setSubmitError(
getAxiosErrorMessage(
e,
'댓글을 등록하지 못했습니다. 잠시 후 다시 시도해 주세요.'
)
)
},
})

const onCommentChange = useCallback((value: string) => {
setSubmitError(null)
setComment(value.slice(0, COMMENT_MAX_LENGTH))
}, [])

const canSubmit =
comment.trim().length > 0 && !mutation.isPending && !attachment.error

const submit = useCallback(() => {
if (!canSubmit) return
setSubmitError(null)
mutation.mutate()
}, [canSubmit, mutation])

// 오래된 → 최신 정렬 (createdAt 오름차순, 대화 흐름)
const comments = [...(query.data ?? [])].sort(
(a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
)

return {
comments,
messageCount: comments.length,
isLoading: query.isPending && query.fetchStatus !== 'idle',
isError: query.isError,
refetch: query.refetch,
comment,
onCommentChange,
attachment,
submitError,
isSubmitting: mutation.isPending,
canSubmit,
submit,
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { useCallback, useState } from 'react'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import useAuthStore from '@/shared/stores/useAuthStore'
import { queryKeys } from '@/shared/lib/queryKeys'
import { getAxiosErrorMessage } from '@/shared/lib/getAxiosErrorMessage'
import {
cancelWorkspaceRequest,
fetchWorkspaceRequestDetail,
} from '@/features/store-register/api/workspaceRequests'

/** 업장 등록 신청 상세 ViewModel — 상세 조회 + 취소 */
export function useStoreRegisterRequestDetailViewModel(requestId: number) {
const queryClient = useQueryClient()
const isLoggedIn = useAuthStore(state => state.isLoggedIn)
const scope = useAuthStore(state => state.scope)

const [cancelError, setCancelError] = useState<string | null>(null)
const [isConfirmOpen, setIsConfirmOpen] = useState(false)

const query = useQuery({
queryKey: queryKeys.storeRegisterRequest.detail(scope, requestId),
queryFn: () => fetchWorkspaceRequestDetail(scope, requestId),
enabled: isLoggedIn && Number.isFinite(requestId),
})

const cancelMutation = useMutation({
mutationFn: () => cancelWorkspaceRequest(scope, requestId),
onSuccess: async () => {
setIsConfirmOpen(false)
await Promise.all([
queryClient.invalidateQueries({
queryKey: queryKeys.storeRegisterRequest.list(scope),
}),
queryClient.invalidateQueries({
queryKey: queryKeys.storeRegisterRequest.detail(scope, requestId),
}),
])
},
onError: (e: unknown) => {
setCancelError(
getAxiosErrorMessage(
e,
'신청을 취소하지 못했습니다. 잠시 후 다시 시도해 주세요.'
)
)
},
})

const openCancelConfirm = useCallback(() => {
setCancelError(null)
setIsConfirmOpen(true)
}, [])

const closeCancelConfirm = useCallback(() => {
if (cancelMutation.isPending) return
setIsConfirmOpen(false)
}, [cancelMutation.isPending])

const confirmCancel = useCallback(() => {
setCancelError(null)
cancelMutation.mutate()
}, [cancelMutation])

return {
scope,
requestId,
detail: query.data ?? null,
isLoading: query.isPending && query.fetchStatus !== 'idle',
isError: query.isError,
isConfirmOpen,
isCanceling: cancelMutation.isPending,
cancelError,
openCancelConfirm,
closeCancelConfirm,
confirmCancel,
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useQuery } from '@tanstack/react-query'
import useAuthStore from '@/shared/stores/useAuthStore'
import { queryKeys } from '@/shared/lib/queryKeys'
import { fetchWorkspaceRequests } from '@/features/store-register/api/workspaceRequests'

/** 업장 등록 신청 내역 목록 ViewModel */
export function useStoreRegisterRequestsViewModel() {
const isLoggedIn = useAuthStore(state => state.isLoggedIn)
const scope = useAuthStore(state => state.scope)

const query = useQuery({
queryKey: queryKeys.storeRegisterRequest.list(scope),
queryFn: () => fetchWorkspaceRequests(scope),
enabled: isLoggedIn,
staleTime: 30_000,
})

return {
requests: query.data ?? [],
isLoading: query.isPending && query.fetchStatus !== 'idle',
isError: query.isError,
refetch: query.refetch,
}
}
Loading
Loading