summaryrefslogtreecommitdiff
path: root/lib/shi-api/shi-api-utils.ts
blob: 3906883aa6bd97dd063c60cd98338f92d3f29e45 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
'use server';

import { nonsapUser, users } from '@/db/schema';
import db from '@/db/db';
import { sql } from 'drizzle-orm';
import { debugError, debugLog, debugWarn, debugSuccess } from '@/lib/debug-utils';

const shiApiBaseUrl = process.env.SHI_API_BASE_URL;
const shiNonsapUserSegment = process.env.SHI_NONSAP_USER_SEGMENT;
const shiApiJwtToken = process.env.SHI_API_JWT_TOKEN;

type NonsapUser = typeof nonsapUser.$inferSelect;
type NonsapUserInsert = typeof nonsapUser.$inferInsert;
type InsertUser = typeof users.$inferInsert;

export const getAllNonsapUser = async () => {
  try{
    debugLog('Starting NONSAP user sync via SHI-API');
    if (!shiApiBaseUrl || !shiNonsapUserSegment || !shiApiJwtToken) {
      throw new Error('SHI API 환경변수가 설정되지 않았습니다. (SHI_API_BASE_URL, SHI_NONSAP_USER_SEGMENT, SHI_API_JWT_TOKEN)');
    }

    const ynToBool = (value: string | null | undefined) => (value || '').toUpperCase() === 'Y';

    // ** 1. 전체 데이터 조회해 응답 받음 (js 배열) **
    const response = await fetch(`${shiApiBaseUrl}${shiNonsapUserSegment}`, {
      headers: {
        Authorization: `Bearer ${shiApiJwtToken}`,
      },
      cache: 'no-store',
    });

    if (!response.ok) {
      const text = await response.text().catch(() => '');
      throw new Error(`SHI-API 요청 실패: ${response.status} ${response.statusText} ${text}`);
    }

    const data: NonsapUser[] = await response.json();
    debugSuccess(`[SHI-API] fetched ${Array.isArray(data) ? data.length : 0} users`);

    // ** 2. 받은 데이터를 DELETE & INSERT 방식으로 수신 테이블 (nonsap-user) 에 저장 **
    await db.delete(nonsapUser); // 전체 정리
    if (Array.isArray(data) && data.length > 0) {
      await db.insert(nonsapUser).values(data as unknown as NonsapUserInsert[]); // 데이터 저장 (스키마 컬럼 그대로)
      debugSuccess(`[STAGE] nonsap_user refreshed with ${data.length} records`);
    }

    // ** 3. 데이터 저장 이후, 비즈니스 테이블인 "public"."users" 에 동기화 시킴 (매핑 필요) **
    const now = new Date();

    const mappedRaw: Partial<InsertUser>[] = (Array.isArray(data) ? data : [])
      .map((u: NonsapUser): Partial<InsertUser> => {
        const isDeleted = ynToBool(u.DEL_YN); // nonsap user 테이블에서 삭제여부
        const isAbsent = ynToBool(u.LOFF_GB); // nonsap user 테이블에서 휴직여부
        const notApproved = (u.AGR_YN || '').toUpperCase() === 'N'; // nonsap user 테이블에서 승인여부
        const isActive = !(isDeleted || isAbsent || notApproved); // eVCP 내에서 활성화 여부
        // S = 정직원
        const isRegularEmployee = (u.REGL_ORORD_GB || '').toUpperCase() === 'S';

        return {          
          // mapped fields
          nonsapUserId: u.USR_ID || undefined,
          employeeNumber: u.EMPNO || undefined,
          knoxId: u.MYSNG_ID || undefined,
          name: u.USR_NM || undefined,
          email: u.EMAIL_ADR || undefined,
          epId: u.MYSNG_ID || undefined,
          deptCode: u.DEPTCD || undefined,
          deptName: u.DEPTNM || undefined,
          phone: u.TELNO || undefined,
          isAbsent,
          isDeletedOnNonSap: isDeleted,
          isActive,
          isRegularEmployee,
        };
      });
      // users 테이블 제약조건 대응: email, name 은 not null + nonsapUserId 존재
      //.filter((u) => typeof u.email === 'string' && !!u.email && typeof u.name === 'string' && !!u.name && typeof u.nonsapUserId === 'string' && u.nonsapUserId.length > 0);

    const mappedUsers = mappedRaw as InsertUser[];

    if (mappedUsers.length > 0) {
      // 배치 처리: 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)');
    }

    // 휴직 사용자도 API에서 수신하므로, 기존 사용자와의 비교를 통한 휴직 처리 로직은 더 이상 필요하지 않음

    return {
      fetched: Array.isArray(data) ? data.length : 0,
      staged: Array.isArray(data) ? data.length : 0,
      upserted: mappedUsers.length,
      skippedDueToMissingRequiredFields: (Array.isArray(data) ? data.length : 0) - mappedUsers.length,
      ranAt: now.toISOString(),
    };
  } catch(error){
    debugError('SHI-API 동기화 실패', error);
    console.error("SHI-API 를 통한 유저 동기화 프로세스 간 실패 발생: ", error);
    throw error;
  }
};