diff options
| -rw-r--r-- | .env.development | 4 | ||||
| -rw-r--r-- | .env.production | 4 | ||||
| -rw-r--r-- | _docker/docker-compose.yml | 17 | ||||
| -rw-r--r-- | components/login/partner-auth-form.tsx | 2 | ||||
| -rw-r--r-- | db/schema/users.ts | 4 | ||||
| -rw-r--r-- | lib/file-stroage.ts | 38 | ||||
| -rw-r--r-- | lib/shi-api/shi-api-utils.ts | 68 | ||||
| -rw-r--r-- | next.config.ts | 11 |
8 files changed, 98 insertions, 50 deletions
diff --git a/.env.development b/.env.development index b4ba1d87..f43928f6 100644 --- a/.env.development +++ b/.env.development @@ -94,8 +94,8 @@ DOLCE_DOWNLOAD_URL="http://60.100.99.217:1111/Download.aspx" ### SHI-API ### # 운영 -# SHI_API_BASE_URL="http://www.qa.shi-api.com" -SHI_API_BASE_URL="http://www.shi-api.com" +# SHI_API_BASE_URL="http://qa.shi-api.com" +SHI_API_BASE_URL="http://shi-api.com" SHI_NONSAP_USER_SEGMENT="/evcp/Common/CMCTB_USR" NONSAP_USERSYNC_FIRST_RUN="true" diff --git a/.env.production b/.env.production index 47459dc3..7b52a1a8 100644 --- a/.env.production +++ b/.env.production @@ -94,8 +94,8 @@ DOLCE_DOWNLOAD_URL="http://60.100.99.217:1111/Download.aspx" ### SHI-API ### # 운영 -# SHI_API_BASE_URL="http://www.qa.shi-api.com" -SHI_API_BASE_URL="http://www.shi-api.com" +# SHI_API_BASE_URL="http://qa.shi-api.com" +SHI_API_BASE_URL="http://shi-api.com" SHI_NONSAP_USER_SEGMENT="/evcp/Common/CMCTB_USR" NONSAP_USERSYNC_FIRST_RUN="true" diff --git a/_docker/docker-compose.yml b/_docker/docker-compose.yml new file mode 100644 index 00000000..62b6bae5 --- /dev/null +++ b/_docker/docker-compose.yml @@ -0,0 +1,17 @@ +# 로컬 DB를 컨테이너로 관리하는 경우 사용 +services: + postgres: + image: postgres:17.2 + container_name: postgres_for_SHI_EVCP # 컨테이너 네임 겹치면 안됨 + environment: + POSTGRES_USER: dts + POSTGRES_PASSWORD: dujinDTS2 + POSTGRES_DB: evcp + ports: + - "5432:5432" # host:container, container는 항상 5432, host 측은 원하는 포트로 설정 + volumes: + - postgres_data:/var/lib/postgresql/data + restart: always + +volumes: + evcp_postgres_data: # 볼륨 네임 겹치면 안됨
\ No newline at end of file diff --git a/components/login/partner-auth-form.tsx b/components/login/partner-auth-form.tsx index 5fed19cf..fb8cd8af 100644 --- a/components/login/partner-auth-form.tsx +++ b/components/login/partner-auth-form.tsx @@ -221,7 +221,7 @@ export function CompanyAuthForm({ className, ...props }: UserAuthFormProps) { <input id="taxid" name="taxid" - placeholder="880-81-01710" + placeholder="000-00-00000" type="text" autoCapitalize="none" autoComplete="off" diff --git a/db/schema/users.ts b/db/schema/users.ts index 9977a442..bfea20a1 100644 --- a/db/schema/users.ts +++ b/db/schema/users.ts @@ -33,7 +33,7 @@ export const users = pgTable("users", { language: varchar("language", { length: 10 }).default("en"), // MFA 관련 새 컬럼들 - phone: varchar("phone", { length: 20 }), // 국제 형식 전화번호 (+82-10-1234-5678) + phone: varchar("phone", { length: 255 }), // 국제 형식 전화번호 (+82-10-1234-5678), 해외번호 20자 초과건으로 사이즈 변경 mfaEnabled: boolean("mfa_enabled").default(false).notNull(), mfaSecret: varchar("mfa_secret", { length: 32 }), // TOTP secret (나중에 사용) @@ -61,7 +61,7 @@ export const users = pgTable("users", { // 김희은 프로 요구사항으로 추가 employeeNumber: varchar("employee_number", { length: 50 }), - knoxId: varchar("knox_id", { length: 50 }), + knoxId: varchar("knox_id", { length: 50 }), // Knox ID로 이메일 앞부분 nonsapUserId: varchar("nonsap_user_id", { length: 50 }).unique(), isAbsent: boolean("is_absent"), // 휴직여부 (SHI-API LOFF_GB (Y/N)) isDeletedOnNonSap: boolean("is_deleted_on_non_sap"), // 퇴직여부 (SHI-API DEL_YN (Y/N)) diff --git a/lib/file-stroage.ts b/lib/file-stroage.ts index c347ffe3..7bc9ef1c 100644 --- a/lib/file-stroage.ts +++ b/lib/file-stroage.ts @@ -194,31 +194,31 @@ class FileSecurityValidator { return { valid: true }; } - // 파일 내용 기본 검증 (매직 넘버 체크 + XSS 패턴 검사) + // 파일 내용 기본 검증 (매직 넘버 체크(비활성화) + XSS 패턴 검사) static async validateFileContent(buffer: Buffer, fileName: string): Promise<{ valid: boolean; error?: string }> { try { const extension = path.extname(fileName).toLowerCase().substring(1); - // 파일 시그니처 (매직 넘버) 검증 - const fileSignatures: Record<string, Buffer[]> = { - 'pdf': [Buffer.from([0x25, 0x50, 0x44, 0x46])], // %PDF - 'jpg': [Buffer.from([0xFF, 0xD8, 0xFF])], - 'jpeg': [Buffer.from([0xFF, 0xD8, 0xFF])], - 'png': [Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A])], - 'gif': [Buffer.from([0x47, 0x49, 0x46, 0x38])], // GIF8 - 'zip': [Buffer.from([0x50, 0x4B, 0x03, 0x04]), Buffer.from([0x50, 0x4B, 0x05, 0x06])], - }; + // 파일 시그니처 (매직 넘버) 검증 << DRM 파일 처리 불가로 주석 처리 + // const fileSignatures: Record<string, Buffer[]> = { + // 'pdf': [Buffer.from([0x25, 0x50, 0x44, 0x46])], // %PDF + // 'jpg': [Buffer.from([0xFF, 0xD8, 0xFF])], + // 'jpeg': [Buffer.from([0xFF, 0xD8, 0xFF])], + // 'png': [Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A])], + // 'gif': [Buffer.from([0x47, 0x49, 0x46, 0x38])], // GIF8 + // 'zip': [Buffer.from([0x50, 0x4B, 0x03, 0x04]), Buffer.from([0x50, 0x4B, 0x05, 0x06])], + // }; - const expectedSignatures = fileSignatures[extension]; - if (expectedSignatures) { - const hasValidSignature = expectedSignatures.some(signature => - buffer.subarray(0, signature.length).equals(signature) - ); + // const expectedSignatures = fileSignatures[extension]; + // if (expectedSignatures) { + // const hasValidSignature = expectedSignatures.some(signature => + // buffer.subarray(0, signature.length).equals(signature) + // ); - if (!hasValidSignature) { - return { valid: false, error: `파일 내용이 확장자와 일치하지 않습니다: ${extension}` }; - } - } + // if (!hasValidSignature) { + // return { valid: false, error: `파일 내용이 확장자와 일치하지 않습니다: ${extension}` }; + // } + // } // 실행 파일 패턴 검색 const executablePatterns = [ diff --git a/lib/shi-api/shi-api-utils.ts b/lib/shi-api/shi-api-utils.ts index 280a2fcb..3906883a 100644 --- a/lib/shi-api/shi-api-utils.ts +++ b/lib/shi-api/shi-api-utils.ts @@ -65,8 +65,8 @@ export const getAllNonsapUser = async () => { name: u.USR_NM || undefined, email: u.EMAIL_ADR || undefined, epId: u.MYSNG_ID || undefined, - deptCode: u.CH_DEPTCD || undefined, - deptName: u.CH_DEPTNM || undefined, + deptCode: u.DEPTCD || undefined, + deptName: u.DEPTNM || undefined, phone: u.TELNO || undefined, isAbsent, isDeletedOnNonSap: isDeleted, @@ -80,27 +80,49 @@ export const getAllNonsapUser = async () => { const mappedUsers = mappedRaw as InsertUser[]; if (mappedUsers.length > 0) { - await db.insert(users) - .values(mappedUsers) - .onConflictDoUpdate({ - target: users.nonsapUserId, - set: { - name: users.name, - employeeNumber: users.employeeNumber, - knoxId: users.knoxId, - epId: users.epId, - deptCode: users.deptCode, - deptName: users.deptName, - phone: users.phone, - nonsapUserId: users.nonsapUserId, - isAbsent: users.isAbsent, - isDeletedOnNonSap: users.isDeletedOnNonSap, - isActive: users.isActive, - isRegularEmployee: users.isRegularEmployee, - updatedAt: sql`now()`, - }, - }); - debugSuccess(`[UPSERT] users upserted=${mappedUsers.length} using key=nonsapUserId`); + // 배치 처리: 500개씩 분할하여 처리 (콜스택 크기 문제 대응) + const BATCH_SIZE = 500; + let totalUpserted = 0; + + for (let i = 0; i < mappedUsers.length; i += BATCH_SIZE) { + const batch = mappedUsers.slice(i, i + BATCH_SIZE); + + try { + await db.insert(users) + .values(batch) + .onConflictDoUpdate({ + target: users.nonsapUserId, + set: { + name: users.name, + employeeNumber: users.employeeNumber, + knoxId: users.knoxId, + epId: users.epId, + deptCode: users.deptCode, + deptName: users.deptName, + phone: users.phone, + nonsapUserId: users.nonsapUserId, + isAbsent: users.isAbsent, + isDeletedOnNonSap: users.isDeletedOnNonSap, + isActive: users.isActive, + isRegularEmployee: users.isRegularEmployee, + updatedAt: sql`now()`, + }, + }); + + totalUpserted += batch.length; + debugLog(`[BATCH ${Math.floor(i/BATCH_SIZE) + 1}] Processed ${batch.length} users (${totalUpserted}/${mappedUsers.length})`); + + // 배치 간 잠시 대기하여 시스템 부하 방지 + if (i + BATCH_SIZE < mappedUsers.length) { + await new Promise(resolve => setTimeout(resolve, 100)); + } + } catch (batchError) { + debugError(`[BATCH ${Math.floor(i/BATCH_SIZE) + 1}] Failed to process batch ${i}-${i+batch.length}`, batchError); + throw batchError; + } + } + + debugSuccess(`[UPSERT] users upserted=${totalUpserted} using key=nonsapUserId (processed in ${Math.ceil(mappedUsers.length/BATCH_SIZE)} batches)`); } else { debugWarn('[UPSERT] No users mapped for upsert (missing name/email or invalid USR_ID)'); } diff --git a/next.config.ts b/next.config.ts index 2a9978a8..44275452 100644 --- a/next.config.ts +++ b/next.config.ts @@ -28,13 +28,22 @@ const nextConfig: NextConfig = { 'tedious': false, }; - // [김준회] handlebars require.extensions 경고 무시 config.ignoreWarnings = [ ...(config.ignoreWarnings || []), + // [김준회] handlebars require.extensions 경고 무시 { module: /node_modules\/handlebars\/lib\/index\.js/, message: /require\.extensions is not supported by webpack/, }, + // [김준회] knex import 관련 경고 무시 (webpack에서 정적 분석 진행할 때, 런타임에 결정되는 경로를 미리 알 수 없어 발생하는 경고임. 실제 동작에는 문제가 없음.) + { + module: /node_modules\/knex/, + message: /Critical dependency: the request of a dependency is an expression/, + }, + { + module: /node_modules\/knex/, + message: /Can't resolve/, + }, ]; return config; |
