diff options
| -rw-r--r-- | atoms.ts | 4 | ||||
| -rw-r--r-- | hooks/use-sync-api.ts | 76 | ||||
| -rw-r--r-- | hooks/use-sync-status.ts | 189 | ||||
| -rw-r--r-- | types/enhanced-documents.d.ts | 107 | ||||
| -rw-r--r-- | types/table.d.ts | 2 |
5 files changed, 376 insertions, 2 deletions
@@ -1,3 +1,5 @@ import { atom } from "jotai" -export const selectedUserIdsAtom = atom<number[]>([])
\ No newline at end of file +export const selectedUserIdsAtom = atom<number[]>([]) +export const selectedModeAtom = atom<"IM" | "ENG">("IM") + diff --git a/hooks/use-sync-api.ts b/hooks/use-sync-api.ts new file mode 100644 index 00000000..18421209 --- /dev/null +++ b/hooks/use-sync-api.ts @@ -0,0 +1,76 @@ +// hooks/use-sync-api.ts (API 호출 전용 훅들) +import useSWR from 'swr' +import useSWRMutation from 'swr/mutation' +import { mutate } from 'swr' +import { ApiClient } from '@/lib/api-utils' + +// 동기화 상태 API 훅 +export function useSyncStatusApi(contractId: number, targetSystem: string = 'SHI') { + const key = contractId ? `sync-status-${contractId}-${targetSystem}` : null + + const { data, error, isLoading } = useSWR( + key, + async () => ApiClient.get('/sync/status', { contractId, targetSystem }), + { + refreshInterval: 30000, + revalidateOnFocus: true, + errorRetryCount: 1, + errorRetryInterval: 5000, + } + ) + + const refetch = () => { + if (key) mutate(key) + } + + return { + syncStatus: data, + isLoading, + error, + refetch + } +} + +// 동기화 트리거 API 훅 +export function useTriggerSyncApi() { + const { trigger, isMutating, error } = useSWRMutation( + 'trigger-sync', + async (key: string, { arg }: { arg: { contractId: number; targetSystem?: string } }) => { + return ApiClient.post('/sync/trigger', arg) + }, + { + onSuccess: (data, key, config) => { + const { contractId, targetSystem = 'SHI' } = config.arg + // 관련 캐시 무효화 + mutate(`sync-status-${contractId}-${targetSystem}`) + mutate(`sync-batches-${contractId}-${targetSystem}`) + }, + } + ) + + return { + triggerSync: trigger, + isLoading: isMutating, + error + } +} + +// 동기화 배치 API 훅 +export function useSyncBatchesApi(contractId: number, targetSystem: string = 'SHI') { + const key = contractId ? `sync-batches-${contractId}-${targetSystem}` : null + + const { data, error, isLoading } = useSWR( + key, + async () => ApiClient.get('/sync/batches', { contractId, targetSystem }), + { + revalidateOnFocus: false, + errorRetryCount: 1, + } + ) + + return { + syncBatches: data, + isLoading, + error + } +}
\ No newline at end of file diff --git a/hooks/use-sync-status.ts b/hooks/use-sync-status.ts new file mode 100644 index 00000000..07cb3432 --- /dev/null +++ b/hooks/use-sync-status.ts @@ -0,0 +1,189 @@ +// hooks/use-sync-status.ts (수정된 버전) +import useSWR from 'swr' +import useSWRMutation from 'swr/mutation' +import { mutate } from 'swr' + +// 단순한 fetcher 함수들 +const fetcher = async (url: string) => { + const response = await fetch(url) + if (!response.ok) { + const error = new Error(`HTTP ${response.status}`) + ;(error as any).status = response.status + throw error + } + return response.json() +} + +// 동기화 상태 조회 +export function useSyncStatus(contractId: number, targetSystem: string = 'SHI') { + const key = contractId + ? `/api/sync/status?contractId=${contractId}&targetSystem=${targetSystem}` + : null + + const { data, error, isLoading } = useSWR( + key, + fetcher, + { + refreshInterval: 30000, // 30초마다 갱신 + revalidateOnFocus: true, + revalidateOnReconnect: true, + shouldRetryOnError: false, // 에러시 자동 재시도 비활성화 + dedupingInterval: 5000, // 5초 내 중복 요청 방지 + } + ) + + const refetch = () => { + if (key) { + mutate(key) + } + } + + return { + syncStatus: data, + isLoading, + error, + refetch + } +} + +// 동기화 배치 목록 조회 +export function useSyncBatches(contractId: number, targetSystem: string = 'SHI') { + const key = contractId + ? `/api/sync/batches?contractId=${contractId}&targetSystem=${targetSystem}` + : null + + const { data, error, isLoading } = useSWR( + key, + fetcher, + { + revalidateOnFocus: false, + shouldRetryOnError: false, + } + ) + + return { + syncBatches: data, + isLoading, + error + } +} + +// 동기화 설정 조회 +export function useSyncConfig(contractId: number, targetSystem: string = 'SHI') { + const key = contractId + ? `/api/sync/config?contractId=${contractId}&targetSystem=${targetSystem}` + : null + + const { data, error, isLoading } = useSWR( + key, + fetcher, + { + revalidateOnFocus: false, + shouldRetryOnError: false, + } + ) + + return { + syncConfig: data, + isLoading, + error + } +} + +// 동기화 트리거 (뮤테이션) +export function useTriggerSync() { + const { trigger, isMutating, error } = useSWRMutation( + '/api/sync/trigger', + async (url: string, { arg }: { arg: { contractId: number; targetSystem?: string } }) => { + const response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(arg) + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + const error = new Error(errorData.message || `HTTP ${response.status}`) + ;(error as any).status = response.status + throw error + } + + return response.json() + }, + { + onSuccess: (data, key, config) => { + // 관련 캐시 무효화 + const { contractId, targetSystem = 'SHI' } = config.arg + const statusKey = `/api/sync/status?contractId=${contractId}&targetSystem=${targetSystem}` + const batchesKey = `/api/sync/batches?contractId=${contractId}&targetSystem=${targetSystem}` + + mutate(statusKey) + mutate(batchesKey) + }, + onError: (error) => { + console.error('Sync trigger failed:', error) + } + } + ) + + return { + triggerSync: trigger, + isLoading: isMutating, + error + } +} + +// 동기화 설정 업데이트 (뮤테이션) +export function useUpdateSyncConfig() { + const { trigger, isMutating, error } = useSWRMutation( + '/api/sync/config', + async (url: string, { arg }: { arg: any }) => { + const response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(arg) + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + const error = new Error(errorData.message || `HTTP ${response.status}`) + ;(error as any).status = response.status + throw error + } + + return response.json() + }, + { + onSuccess: (data, key, config) => { + // 설정 캐시 무효화 + const { contractId, targetSystem } = config.arg + const configKey = `/api/sync/config?contractId=${contractId}&targetSystem=${targetSystem}` + mutate(configKey) + }, + } + ) + + return { + updateConfig: trigger, + isLoading: isMutating, + error + } +} + +// 실시간 동기화 상태 훅 (높은 갱신 빈도) +export function useRealtimeSyncStatus(contractId: number, targetSystem: string = 'SHI') { + const key = contractId + ? `/api/sync/status?contractId=${contractId}&targetSystem=${targetSystem}&realtime=true` + : null + + return useSWR( + key, + fetcher, + { + refreshInterval: 5000, // 5초마다 갱신 (실시간) + revalidateOnFocus: true, + revalidateOnReconnect: true, + shouldRetryOnError: false, + } + ) +}
\ No newline at end of file diff --git a/types/enhanced-documents.d.ts b/types/enhanced-documents.d.ts new file mode 100644 index 00000000..99222db3 --- /dev/null +++ b/types/enhanced-documents.d.ts @@ -0,0 +1,107 @@ +// types/enhanced-documents.ts +import { type InferSelectModel } from "drizzle-orm" +import { documents, issueStages, revisions, documentAttachments, enhancedDocumentsView } from "@/db/schema/vendorDocu" + +// DB 스키마에서 추출한 기본 타입들 +export type Document = InferSelectModel<typeof documents> +export type IssueStage = InferSelectModel<typeof issueStages> +export type Revision = InferSelectModel<typeof revisions> +export type DocumentAttachment = InferSelectModel<typeof documentAttachments> +export type EnhancedDocument = InferSelectModel<typeof enhancedDocumentsView> + +// 확장된 스테이지 타입 (리비전과 첨부파일 포함) +export type StageWithRevisions = IssueStage & { + revisions: Array<Revision & { + attachments: DocumentAttachment[] + }> +} + +// 완전한 문서 타입 (모든 관련 데이터 포함) +export type FullDocument = Document & { + stages: StageWithRevisions[] + currentStage?: IssueStage + latestRevision?: Revision +} + +// 컴포넌트에서 사용할 확장된 문서 타입 (EnhancedDocument와 호환) +export type EnhancedDocumentWithStages = EnhancedDocument & { + // EnhancedDocument가 이미 documentId를 가지고 있는지 확인 + documentId: number + allStages?: Array<{ + id: number + stageName: string + stageStatus: string + stageOrder: number + planDate: string | null + actualDate: string | null + assigneeName: string | null + priority: string + }> +} + +// 서버 액션용 입력 타입들 +export type CreateDocumentInput = { + contractId: number + docNumber: string + title: string + pic?: string + issuedDate?: string +} + +export type UpdateDocumentInput = Partial<CreateDocumentInput> & { + id: number +} + +export type CreateStageInput = { + documentId: number + stageName: string + planDate?: string + stageOrder?: number + priority?: 'HIGH' | 'MEDIUM' | 'LOW' + assigneeId?: number + assigneeName?: string + description?: string + reminderDays?: number +} + +export type UpdateStageInput = Partial<CreateStageInput> & { + id: number +} + +export type CreateRevisionInput = { + issueStageId: number + revision: string + uploaderType: 'vendor' | 'client' | 'contractor' + uploaderId?: number + uploaderName: string + comment?: string + attachments?: Array<{ + fileName: string + filePath: string + fileType?: string + fileSize: number + }> +} + +export type UpdateRevisionStatusInput = { + id: number + revisionStatus: 'SUBMITTED' | 'UNDER_REVIEW' | 'APPROVED' | 'REJECTED' | 'SUPERSEDED' + reviewerId?: number + reviewerName?: string + reviewComments?: string +} + +// API 응답 타입들 +export type ApiResponse<T> = { + success: boolean + data?: T + error?: string + message?: string +} + +export type PaginatedResponse<T> = { + data: T[] + total: number + pageCount: number + currentPage: number +}
\ No newline at end of file diff --git a/types/table.d.ts b/types/table.d.ts index 5d617159..f98d4c99 100644 --- a/types/table.d.ts +++ b/types/table.d.ts @@ -54,7 +54,7 @@ export type Filter<TData> = Prettify< export interface DataTableRowAction<TData> { row: Row<TData> - type: "addInfo"| "view-series"|"log"| "tbeResult" | "requestInfo"| "esign-detail"| "responseDetail"|"signature"|"update" | "delete" | "user" | "pemission" | "invite" | "items" | "attachment" |"comments" | "open" | "select" | "files" + type: "schedule"| "view"| "upload" | "addInfo"| "view-series"|"log"| "tbeResult" | "requestInfo"| "esign-detail"| "responseDetail"|"signature"|"update" | "delete" | "user" | "pemission" | "invite" | "items" | "attachment" |"comments" | "open" | "select" | "files" } export interface QueryBuilderOpts { |
