summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-08-18 01:06:37 +0000
committerjoonhoekim <26rote@gmail.com>2025-08-18 01:06:37 +0000
commitb36330c94400f39ec9baed64b82a73bae58edbf2 (patch)
treeeea344d39b3101289ed4f6b74e0c98b6b33ef460
parent5adc1df95f80fbec7a0b5bbee15448b10d5aec3a (diff)
(김준회) 파일업로드시 바이너리검증부 주석처리, knex webpack 한계로 인한 빌드 로그 경고 무시, 환경변수 오류 처리, 벤더 로그인 폼 사업자번호를 000 으로 수정, 유저 phone number 사이즈 늘림, 도커 DB 예시 추가
-rw-r--r--.env.development4
-rw-r--r--.env.production4
-rw-r--r--_docker/docker-compose.yml17
-rw-r--r--components/login/partner-auth-form.tsx2
-rw-r--r--db/schema/users.ts4
-rw-r--r--lib/file-stroage.ts38
-rw-r--r--lib/shi-api/shi-api-utils.ts68
-rw-r--r--next.config.ts11
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;