diff options
| -rw-r--r-- | db/schema/SWP/swp-documents.ts | 220 | ||||
| -rw-r--r-- | db/schema/index.ts | 3 | ||||
| -rw-r--r-- | lib/auth/custom-signout.ts | 78 |
3 files changed, 72 insertions, 229 deletions
diff --git a/db/schema/SWP/swp-documents.ts b/db/schema/SWP/swp-documents.ts deleted file mode 100644 index c7661ff6..00000000 --- a/db/schema/SWP/swp-documents.ts +++ /dev/null @@ -1,220 +0,0 @@ -import { - varchar, - timestamp, - serial, - uniqueIndex, - index, - pgEnum, - pgSchema, -} from "drizzle-orm/pg-core"; - -// ============================================================================ -// 스키마 -// ============================================================================ - -export const swpSchema = pgSchema("swp"); - -// ============================================================================ -// ENUMS -// ============================================================================ - -export const syncStatusEnum = pgEnum("swp_sync_status", [ - "synced", - "pending", - "error", -]); - -// ============================================================================ -// 문서 마스터 (GetVDRDocumentList) -// 컬럼명: API 필드명과 동일하게 유지 (UPPER_SNAKE_CASE) -// ============================================================================ - -export const swpDocuments = swpSchema.table( - "swp_documents", - { - // Composite Primary Key: DOC_NO + PROJ_NO - DOC_NO: varchar("DOC_NO", { length: 1000 }).notNull(), - PROJ_NO: varchar("PROJ_NO", { length: 1000 }).notNull(), - - // 문서 기본 정보 - DOC_TITLE: varchar("DOC_TITLE", { length: 1000 }).notNull(), - DOC_GB: varchar("DOC_GB", { length: 1000 }), - DOC_TYPE: varchar("DOC_TYPE", { length: 1000 }), - OWN_DOC_NO: varchar("OWN_DOC_NO", { length: 1000 }), - SHI_DOC_NO: varchar("SHI_DOC_NO", { length: 1000 }), - - // 프로젝트 정보 - PROJ_NM: varchar("PROJ_NM", { length: 1000 }), - PKG_NO: varchar("PKG_NO", { length: 1000 }), - - // 자재/기술 정보 - MAT_CD: varchar("MAT_CD", { length: 1000 }), - MAT_NM: varchar("MAT_NM", { length: 1000 }), - DISPLN: varchar("DISPLN", { length: 1000 }), - CTGRY: varchar("CTGRY", { length: 1000 }), - - // 업체 정보 - VNDR_CD: varchar("VNDR_CD", { length: 1000 }), - CPY_CD: varchar("CPY_CD", { length: 1000 }), - CPY_NM: varchar("CPY_NM", { length: 1000 }), - - // 담당자 정보 - PIC_NM: varchar("PIC_NM", { length: 1000 }), - PIC_DEPTCD: varchar("PIC_DEPTCD", { length: 1000 }), - PIC_DEPTNM: varchar("PIC_DEPTNM", { length: 1000 }), - - // 최신 리비전 정보 (빠른 조회용) - LTST_REV_NO: varchar("LTST_REV_NO", { length: 1000 }), - LTST_REV_SEQ: varchar("LTST_REV_SEQ", { length: 1000 }), - LTST_ACTV_STAT: varchar("LTST_ACTV_STAT", { length: 1000 }), - - // 기타 - STAGE: varchar("STAGE", { length: 1000 }), - SKL_CD: varchar("SKL_CD", { length: 1000 }), - MOD_TYPE: varchar("MOD_TYPE", { length: 1000 }), - ACT_TYPE_NM: varchar("ACT_TYPE_NM", { length: 1000 }), - USE_YN: varchar("USE_YN", { length: 1000 }), - - // 이력 정보 (SWP) - CRTER: varchar("CRTER", { length: 1000 }), - CRTE_DTM: varchar("CRTE_DTM", { length: 1000 }), - CHGR: varchar("CHGR", { length: 1000 }), - CHG_DTM: varchar("CHG_DTM", { length: 1000 }), - REV_DTM: varchar("REV_DTM", { length: 1000 }), - - // 동기화 메타데이터 - sync_status: syncStatusEnum("sync_status").default("synced").notNull(), - last_synced_at: timestamp("last_synced_at").defaultNow().notNull(), - created_at: timestamp("created_at").defaultNow().notNull(), - updated_at: timestamp("updated_at").defaultNow().notNull(), - }, - (table) => ({ - // Composite Primary Key - pk: uniqueIndex("swp_documents_pk").on(table.DOC_NO, table.PROJ_NO), - // Indexes - projNoIdx: index("swp_documents_proj_no_idx").on(table.PROJ_NO), - vndrCdIdx: index("swp_documents_vndr_cd_idx").on(table.VNDR_CD), - pkgNoIdx: index("swp_documents_pkg_no_idx").on(table.PKG_NO), - syncStatusIdx: index("swp_documents_sync_status_idx").on(table.sync_status), - }) -); - -// ============================================================================ -// 문서 리비전 (GetExternalInboxList에서 추출) -// 컬럼명: API 필드명과 동일하게 유지 (UPPER_SNAKE_CASE) -// ============================================================================ - -export const swpDocumentRevisions = swpSchema.table( - "swp_document_revisions", - { - // Primary Key - id: serial("id").primaryKey(), - - // Document Reference (NO FK - 외래키 제거) - DOC_NO: varchar("DOC_NO", { length: 1000 }).notNull(), - - // 리비전 정보 - REV_NO: varchar("REV_NO", { length: 1000 }).notNull(), - STAGE: varchar("STAGE", { length: 1000 }).notNull(), - - // Activity 정보 - ACTV_NO: varchar("ACTV_NO", { length: 1000 }), - ACTV_SEQ: varchar("ACTV_SEQ", { length: 1000 }), - BOX_SEQ: varchar("BOX_SEQ", { length: 1000 }), - OFDC_NO: varchar("OFDC_NO", { length: 1000 }), - - // 프로젝트/패키지 정보 (파일 API에서만 제공) - PROJ_NO: varchar("PROJ_NO", { length: 1000 }), - PKG_NO: varchar("PKG_NO", { length: 1000 }), - VNDR_CD: varchar("VNDR_CD", { length: 1000 }), - CPY_CD: varchar("CPY_CD", { length: 1000 }), - - // 동기화 메타데이터 - sync_status: syncStatusEnum("sync_status").default("synced").notNull(), - last_synced_at: timestamp("last_synced_at").defaultNow().notNull(), - created_at: timestamp("created_at").defaultNow().notNull(), - updated_at: timestamp("updated_at").defaultNow().notNull(), - }, - (table) => ({ - // Unique constraint: 문서당 리비전은 유일 - docRevUnique: uniqueIndex("swp_doc_rev_unique_idx").on( - table.DOC_NO, - table.REV_NO - ), - docNoIdx: index("swp_revisions_doc_no_idx").on(table.DOC_NO), - revNoIdx: index("swp_revisions_rev_no_idx").on(table.REV_NO), - stageIdx: index("swp_revisions_stage_idx").on(table.STAGE), - }) -); - -// ============================================================================ -// 첨부파일 (GetExternalInboxList) -// 컬럼명: API 필드명과 동일하게 유지 (UPPER_SNAKE_CASE) -// ============================================================================ - -export const swpDocumentFiles = swpSchema.table( - "swp_document_files", - { - // Primary Key - id: serial("id").primaryKey(), - - // Foreign Key - revision_id: serial("revision_id") - .notNull() - .references(() => swpDocumentRevisions.id, { onDelete: "cascade" }), - - // 파일 정보 - FILE_NM: varchar("FILE_NM", { length: 1000 }).notNull(), - FILE_SEQ: varchar("FILE_SEQ", { length: 1000 }).notNull(), - FILE_SZ: varchar("FILE_SZ", { length: 1000 }), - FLD_PATH: varchar("FLD_PATH", { length: 1000 }), - - // 문서 참조 (조회 편의용, 비정규화) - DOC_NO: varchar("DOC_NO", { length: 1000 }).notNull(), - - // 상태 정보 - STAT: varchar("STAT", { length: 1000 }), - STAT_NM: varchar("STAT_NM", { length: 1000 }), - IDX: varchar("IDX", { length: 1000 }), - - // Activity 정보 - ACTV_NO: varchar("ACTV_NO", { length: 1000 }), - - // 이력 정보 (SWP) - CRTER: varchar("CRTER", { length: 1000 }), - CRTE_DTM: varchar("CRTE_DTM", { length: 1000 }), - CHGR: varchar("CHGR", { length: 1000 }), - CHG_DTM: varchar("CHG_DTM", { length: 1000 }), - - // 동기화 메타데이터 - sync_status: syncStatusEnum("sync_status").default("synced").notNull(), - last_synced_at: timestamp("last_synced_at").defaultNow().notNull(), - created_at: timestamp("created_at").defaultNow().notNull(), - updated_at: timestamp("updated_at").defaultNow().notNull(), - }, - (table) => ({ - // Unique constraint: 리비전당 파일 시퀀스는 유일 - revFileUnique: uniqueIndex("swp_rev_file_unique_idx").on( - table.revision_id, - table.FILE_SEQ - ), - revisionIdIdx: index("swp_files_revision_id_idx").on(table.revision_id), - docNoIdx: index("swp_files_doc_no_idx").on(table.DOC_NO), - fileNmIdx: index("swp_files_file_nm_idx").on(table.FILE_NM), - }) -); - -// ============================================================================ -// TYPES -// ============================================================================ - -export type SwpDocument = typeof swpDocuments.$inferSelect; -export type SwpDocumentInsert = typeof swpDocuments.$inferInsert; - -export type SwpDocumentRevision = typeof swpDocumentRevisions.$inferSelect; -export type SwpDocumentRevisionInsert = - typeof swpDocumentRevisions.$inferInsert; - -export type SwpDocumentFile = typeof swpDocumentFiles.$inferSelect; -export type SwpDocumentFileInsert = typeof swpDocumentFiles.$inferInsert; - diff --git a/db/schema/index.ts b/db/schema/index.ts index ea39ae8c..85258371 100644 --- a/db/schema/index.ts +++ b/db/schema/index.ts @@ -84,6 +84,3 @@ export * from './avl/vendor-pool'; // === Email Logs 스키마 === export * from './emailLogs'; export * from './emailWhitelist'; - -// SWP 문서/첨부파일 테이블 및 뷰 스키마 -export * from './SWP/swp-documents';
\ No newline at end of file diff --git a/lib/auth/custom-signout.ts b/lib/auth/custom-signout.ts index d59bd81c..6f3a6b01 100644 --- a/lib/auth/custom-signout.ts +++ b/lib/auth/custom-signout.ts @@ -11,19 +11,28 @@ interface CustomSignOutOptions { /** * 커스텀 로그아웃 함수 * - * @param options - callbackUrl: 로그아웃 후 이동할 URL (기본: 현재 origin + "/") + * @param options - callbackUrl: 로그아웃 후 이동할 URL (상대 경로 권장: "/ko/partners") * @param options - redirect: 자동 리다이렉트 여부 (기본: true) */ export async function customSignOut(options?: CustomSignOutOptions): Promise<void> { const { callbackUrl, redirect = true } = options || {}; + console.log('[customSignOut] 시작:', { + currentOrigin: window.location.origin, + currentHref: window.location.href, + callbackUrl, + redirect, + }); + try { // 1. CSRF 토큰 가져오기 const csrfResponse = await fetch('/api/auth/csrf'); const { csrfToken } = await csrfResponse.json(); - // 2. 서버에 로그아웃 요청 - await fetch('/api/auth/signout', { + console.log('[customSignOut] CSRF 토큰 획득'); + + // 2. 서버에 로그아웃 요청 (리다이렉트 방지) + const signoutResponse = await fetch('/api/auth/signout', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', @@ -32,18 +41,75 @@ export async function customSignOut(options?: CustomSignOutOptions): Promise<voi csrfToken, json: 'true', }), + redirect: 'manual', // ⭐ 서버의 리다이렉트를 자동으로 따라가지 않음 + }); + + console.log('[customSignOut] 서버 응답:', { + status: signoutResponse.status, + statusText: signoutResponse.statusText, + redirected: signoutResponse.redirected, + url: signoutResponse.url, }); - // 3. 리다이렉트 + // 3. NextAuth 세션 쿠키 즉시 삭제 (middleware가 감지하도록) + document.cookie = 'next-auth.session-token=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;'; + document.cookie = '__Secure-next-auth.session-token=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT; Secure;'; + + console.log('[customSignOut] 세션 쿠키 삭제 완료'); + + // 4. 리다이렉트 if (redirect) { - const finalUrl = callbackUrl || window.location.origin; + // ⭐ URL 객체로 변환하여 상대 경로인지 확인 + let finalUrl: string; + + if (callbackUrl) { + try { + // callbackUrl이 절대 URL인 경우 (http:// 또는 https://로 시작) + if (callbackUrl.startsWith('http://') || callbackUrl.startsWith('https://')) { + const urlObj = new URL(callbackUrl); + // 같은 origin인 경우 pathname만 사용 (상대 경로로 변환) + if (urlObj.origin === window.location.origin) { + finalUrl = urlObj.pathname + urlObj.search + urlObj.hash; + } else { + finalUrl = callbackUrl; + } + } else { + // 이미 상대 경로인 경우 (/, /ko/partners 등) + finalUrl = callbackUrl; + } + } catch (error) { + console.error('[customSignOut] callbackUrl 파싱 오류:', error); + finalUrl = callbackUrl; // 오류 시 원본 사용 + } + } else { + // callbackUrl이 없는 경우 루트 경로로 + finalUrl = '/'; + } + + console.log('[customSignOut] 리다이렉트 실행:', finalUrl); window.location.href = finalUrl; } } catch (error) { console.error('Custom sign out error:', error); // 에러 발생 시에도 리다이렉트 (세션이 이미 만료되었을 수 있음) if (redirect) { - const finalUrl = callbackUrl || window.location.origin; + let finalUrl = '/'; + if (callbackUrl) { + try { + if (callbackUrl.startsWith('http://') || callbackUrl.startsWith('https://')) { + const urlObj = new URL(callbackUrl); + if (urlObj.origin === window.location.origin) { + finalUrl = urlObj.pathname + urlObj.search + urlObj.hash; + } else { + finalUrl = callbackUrl; + } + } else { + finalUrl = callbackUrl; + } + } catch { + finalUrl = callbackUrl; + } + } window.location.href = finalUrl; } } |
