summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--atoms.ts4
-rw-r--r--hooks/use-sync-api.ts76
-rw-r--r--hooks/use-sync-status.ts189
-rw-r--r--types/enhanced-documents.d.ts107
-rw-r--r--types/table.d.ts2
5 files changed, 376 insertions, 2 deletions
diff --git a/atoms.ts b/atoms.ts
index 54847ca7..fe5cebd3 100644
--- a/atoms.ts
+++ b/atoms.ts
@@ -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 {