들어가며 🚪

자바스크립트로 개발을 하다 보면 객체나 배열을 복사해야 하는 상황이 자주 발생합니다. 특히 React나 Node.js 애플리케이션에서는 데이터의 불변성(immutability)을 유지하기 위해 원본 데이터를 직접 수정하지 않고 복사본을 만들어 작업하는 것이 중요합니다. 이때 '얕은 복사(Shallow Copy)'와 '깊은 복사(Deep Copy)'의 차이를 이해하는 것이 매우 중요한데요, 이 두 개념을 쉽게 이해해 봅시다! 👨‍💻👩‍💻

 

원시 타입 vs 참조 타입 💡

먼저, 자바스크립트의 데이터 타입을 이해해야 합니다

원시 타입(Primitive Types) 📌

  • String, Number, Boolean, null, undefined, Symbol, BigInt
  • 값 자체가 변수에 저장됨
  • 복사할 때 값이 그대로 새 변수에 복사됨

참조 타입(Reference Types) 🔗

  • Object, Array, Function
  • 메모리 주소(참조)가 변수에 저장됨
  • 복사할 때 주소값만 복사되어 같은 데이터를 가리키게 됨

바로 여기서 얕은 복사와 깊은 복사의 차이가 발생합니다!

 

얕은 복사(Shallow Copy) 란? 🌊

얕은 복사는 객체의 최상위 속성들만 새로운 메모리에 복사하고, 중첩된 객체는 여전히 원본 객체의 참조를 유지하는 복사 방법입니다.

얕은 복사의 특징 🏷️

  • 최상위 속성만 새로운 메모리에 복사됨
  • 중첩된 객체나 배열은 참조만 복사됨
  • 중첩된 객체를 수정하면 원본도 영향을 받음

얕은 복사 방법들 🛠️

1. Object.assign() 사용하기

const original = { name: "철수", scores: [90, 85, 95] };
const copied = Object.assign({}, original);

copied.name = "영희"; // OK: 최상위 속성 변경은 원본에 영향 없음
copied.scores.push(100); // 주의: 중첩 객체 변경은 원본도 변경됨!
console.log(original.scores); // [90, 85, 95, 100] 😱

2. 스프레드 연산자(...) 사용하기

const original = { user: { name: "철수", age: 25 }, active: true };
const copied = { ...original };

copied.active = false; // OK: 최상위 속성만 독립적으로 변경
copied.user.age = 26; // 주의: user 객체는 여전히 참조로 연결됨!
console.log(original.user.age); // 26 😱

3. 배열의 얕은 복사

const originalArray = [1, 2, { id: 1, value: "a" }];
const copiedArray = [...originalArray]; // Array.slice()도 동일하게 작동

copiedArray[0] = 99; // OK: 원시값 변경은 독립적
copiedArray[2].value = "b"; // 주의: 객체 참조는 공유됨!
console.log(originalArray[2].value); // "b" 😱

 

깊은 복사(Deep Copy) 란? 🏊‍♂️

깊은 복사는 객체의 모든 수준의 속성을 새로운 메모리에 완전히 복사하는 방법입니다. 중첩된 객체까지 모두 새로운 참조를 갖게 됩니다.

깊은 복사의 특징 📝

  • 모든 중첩 수준의 객체가 새롭게 생성됨
  • 원본 객체와 완전히 독립적인 사본이 만들어짐
  • 어느 한쪽을 수정해도 다른 쪽에 영향을 주지 않음

 

깊은 복사 방법들 🔧

1. JSON.parse() + JSON.stringify() 사용하기

const original = { name: "철수", scores: [90, 85, 95], info: { age: 20 } };
const deepCopied = JSON.parse(JSON.stringify(original));

deepCopied.scores.push(100); // 안전: 중첩 배열도 독립적
deepCopied.info.age = 21; // 안전: 중첩 객체도 독립적
console.log(original.scores); // [90, 85, 95] ✅
console.log(original.info.age); // 20 ✅

2. lodash의 cloneDeep 사용하기 (라이브러리 사용)

import _ from 'lodash';

const original = { 
  user: { name: "철수", hobbies: ["축구", "게임"] }, 
  created: new Date() 
};
const deepCopied = _.cloneDeep(original);

// 모든 수준의 변경이 안전함!
deepCopied.user.hobbies.push("코딩");
console.log(original.user.hobbies); // ["축구", "게임"] ✅

3. React에서 중첩 객체 복사하기 (함수형 접근)

// React에서 상태 업데이트 시 중첩 객체 안전하게 수정하기
const updateUserHobby = (user) => {
  return {
    ...user,
    hobbies: [...user.hobbies, "코딩"],
    info: { ...user.info, age: user.info.age + 1 }
  };
};

 

JSON 방식의 깊은 복사 주의점 ⚠️

JSON을 이용한 깊은 복사는 간단하지만 몇 가지 제한이 있습니다

  • 함수, Map, Set, Date 객체 등은 제대로 복사되지 않음
  • undefined와 같은 특수값이 손실될 수 있음
  • 순환 참조(Circular Reference)가 있으면 오류 발생

실전 사용 예시: React에서 중첩된 상태 업데이트 🔄

React에서는 상태의 불변성을 유지하는 것이 중요합니다. 다음은 중첩된 객체를 안전하게 업데이트하는 예시입니다:

function ProfileEditor() {
  const [user, setUser] = useState({
    name: "김철수",
    contact: { email: "chulsoo@example.com", phone: "010-1234-5678" },
    tags: ["개발자", "리액트"]
  });

  // 중첩된 속성 업데이트 (깊은 복사 원리 활용)
  const updateEmail = (newEmail) => {
    setUser({
      ...user,                      // 1단계 얕은 복사
      contact: {                    // contact 객체 새로 생성
        ...user.contact,            // 기존 contact 속성 복사
        email: newEmail             // email만 업데이트
      }
    });
  };
}

 

얕은 복사 vs 깊은 복사: 언제 무엇을 사용할까? 🤔

얕은 복사가 적합한 경우

  • 단순한 구조의 객체를 다룰 때
  • 성능이 중요하고 중첩 객체를 수정할 일이 없을 때
  • 의도적으로 참조를 공유하고 싶을 때

깊은 복사가 적합한 경우

  • 복잡한 중첩 구조의 객체를 안전하게 복사할 때
  • 원본 데이터의 불변성을 완벽하게 유지해야 할 때
  • React, Redux 등에서 상태 관리 시

정리 📝

  • 얕은 복사: 최상위 속성만 새로 복사, 중첩 객체는 참조 공유 🌊
  • 깊은 복사: 모든 중첩 수준까지 완전히 새로 복사 🏊‍♂️
  • 주의점: 복사 방법에 따라 사이드 이펙트가 발생할 수 있음 ⚠️

자바스크립트에서 객체와 배열을 다룰 때, 얕은 복사와 깊은 복사의 차이를 이해하는 것은 버그를 예방하고 코드의 예측 가능성을 높이는 데 매우 중요합니다. 특히 React나 Redux와 같은 프레임워크에서는 상태의 불변성을 유지하기 위해 이 개념을 제대로 이해하고 있어야 합니다! 💪

728x90

cron과 node-schedule이란?

cronnode-schedule은 Node.js 애플리케이션에서 정해진 시간에 특정 작업을 실행할 수 있게 해주는 스케줄링 라이브러리입니다. 이메일 발송, 데이터베이스 백업, 알림 전송 등 주기적으로 수행해야 하는 작업에 매우 유용합니다.

주요 차이점

1. 문법과 사용 방식

cron은 유닉스 crontab 문법을 직접적으로 사용합니다:

const cron = require('cron');

// 매일 오전 10시 30분에 실행
const job = new cron.CronJob('30 10 * * *', function() {
  console.log('매일 오전 10시 30분에 실행되는 작업입니다.');
});

job.start();

node-schedule은 더 유연한 자바스크립트 객체 기반 문법을 제공합니다:

const schedule = require('node-schedule');

// 매일 오전 10시 30분에 실행
const job = schedule.scheduleJob('30 10 * * *', function() {
  console.log('매일 오전 10시 30분에 실행되는 작업입니다.');
});

2. 기능의 차이

node-schedule:

  • 날짜 객체를 직접 사용할 수 있음
  • 복잡한 스케줄링 규칙 지원
  • 작업 취소 및 재스케줄링이 편리함

cron:

  • 더 가벼운 라이브러리
  • 유닉스 crontab 문법에 익숙한 사용자에게 직관적
  • 타임존 지원이 내장되어 있음

3. 날짜 객체 사용 예시

node-schedule에서는 Date 객체를 직접 사용할 수 있습니다:

const schedule = require('node-schedule');

// 특정 날짜와 시간에 실행
const date = new Date(2025, 2, 20, 15, 30, 0);
const job = schedule.scheduleJob(date, function() {
  console.log('2025년 3월 20일 오후 3시 30분에 실행됩니다.');
});

cron에서는 Date 객체를 직접 사용할 수 없고, cron 표현식을 사용해야 합니다:

const cron = require('cron');

// 특정 날짜와 시간을 cron 표현식으로 표현해야 함
const job = new cron.CronJob('0 30 15 20 3 *', function() {
  console.log('2025년 3월 20일 오후 3시 30분에 실행됩니다.');
}, null, true, 'Asia/Seoul');

실제 사용 예시: 일일 보고서 생성

cron을 사용한 예시

const cron = require('cron');
const fs = require('fs');

// 매일 밤 12시에 보고서 생성
const dailyReport = new cron.CronJob('0 0 0 * * *', function() {
  const today = new Date();
  const reportData = `일일 보고서 - ${today.toLocaleDateString()}`;
  
  fs.writeFile(`report-${today.toISOString().split('T')[0]}.txt`, reportData, (err) => {
    if (err) throw err;
    console.log('일일 보고서가 생성되었습니다.');
  });
}, null, true, 'Asia/Seoul');

dailyReport.start();

node-schedule을 사용한 예시

const schedule = require('node-schedule');
const fs = require('fs');

// 매일 밤 12시에 보고서 생성
const dailyReport = schedule.scheduleJob('0 0 0 * * *', function() {
  const today = new Date();
  const reportData = `일일 보고서 - ${today.toLocaleDateString()}`;
  
  fs.writeFile(`report-${today.toISOString().split('T')[0]}.txt`, reportData, (err) => {
    if (err) throw err;
    console.log('일일 보고서가 생성되었습니다.');
  });
});

어떤 라이브러리를 선택해야 할까요?

cron을 선택하면 좋은 경우:

  • 유닉스 crontab 문법에 익숙한 경우
  • 타임존 지원이 중요한 경우
  • 가벼운 라이브러리가 필요한 경우

node-schedule을 선택하면 좋은 경우:

  • 날짜 객체로 직접 스케줄링하고 싶은 경우
  • 복잡한 스케줄링 규칙이 필요한 경우
  • 작업 취소나 재스케줄링이 자주 필요한 경우

결론

두 라이브러리 모두 Node.js에서 작업 스케줄링을 위한 훌륭한 도구입니다. 간단한 주기적 작업이라면 어떤 라이브러리를 선택하든 큰 차이가 없지만, 프로젝트의 특성과 개발자의 선호도에 따라 선택하면 됩니다. cron은 유닉스 스타일의 간결한 문법을 제공하고, node-schedule은 더 유연하고 자바스크립트 친화적인 인터페이스를 제공합니다.

초보자의 경우, 자바스크립트 객체를 직접 다룰 수 있는 node-schedule이 조금 더 이해하기 쉬울 수 있습니다. 하지만 장기적으로는 두 라이브러리의 문법을 모두 알아두면 다양한 상황에 대응할 수 있습니다.

728x90

# 기존방법

- Slack Webhooks를 통해 Slack채널로 HTTP 요청

- Icoming Webhooks: 가장 간단한 방법중 하나로 Slack에서 발급된 URL로 HTTP POST 요청을 보냄

- Slack 웹훅을 사용하기 위해선 앱을 생성해야함

 

https://velog.io/@sssssssssy/nextjs-slack-webhooks

 

Next.js와 Slack Webhooks로 실시간 에러 알림 시스템 만들기

[FE] Next.js + Slack Webhooks 실시간 에러 알림 설정

velog.io

위의 링크를 통해 개발에 필요한 bot 생성 및 토큰 발행에 큰 도움을 받았다 (+git을 통해 기초코드까지 있음)

 

# 문자 발송은 바로 되지만 파일은 실패!

- 메시지 발송하는 방법은 위 링크에서 git을 통해 받은 코드와 설명을 읽은 후 바로 진행됨

- 그러나 여러가지 문제로 계속 실패 (특히, 권한 문제)

 

# 다양한 에러코드

1. not_int_channal

- 해당 에러코드는 내가 전달하고 싶은 Slack 채널에서 /invite @봇이름 을 하면 해결

 

2. method_deprecated

- 무슨 뜻이야..? 검색해보니 "사용되지 않는 메서드"

- 검색해보면 files.upload 를 사용하여 다 구현했다고 하는데 왜 이런 에러가?

- 공식 API문서를 보면 "files.upload는 2025년 3월에 종료되며 순차적인 웹 API 메서드로 대체됩니다."

- 코드 작성할때가 25년 3월 14일인데, 3월 11일에 종료되었다고 한다ㅠㅠ

이게 외 않대?

- 그럼 뭐 써요???

-  files.getUploadURLExternal 와 files.completeUploadExternal를 쓰라고 함

- 왜 파일업로드가 2개로 나누어졌지?

- files.getUploadURLExternal : 외부 파일 업로드에 대한 URL을 가져옵니다.

- files.completeUploadExternal:  files.getUploadURLExternal로 시작된 업로드를 완료합니다.

- 그럼 이걸로 시작해볼까?

 

 

3. missing_scope

- 메소드를 변경하니 새로운 문제 발생

- 권한을 추가해야한다는 뜻. slack api 웹페이지에서 OAuth & Permissions 메뉴를 들어간다.

- Scopes에서 Bot과 User의 Scopes를 설정할 수 있음

- 아마 필요한건 channels:read, chat:write, files:read, files:write로 생각됨

뭐가 필요한지 몰라서 다넣어봄

 

4. invalid_arguments

- '유효하지 않은 인자'

- 무엇을 잘못넣은거지....? 답은 공식 문서에 있다

 

5. 그래도 안됨ㅠㅠ

더보기
  • 파일 정보 읽기: fs.promises.stat과 fs.promises.readFile을 사용하여 파일 정보와 내용을 읽습니다.
  • 업로드 URL 얻기: files.getUploadURLExternal API를 호출하여 파일을 업로드할 URL을 얻습니다.
  • 파일 업로드: 얻은 URL에 PUT 요청으로 파일을 업로드합니다. 여기서 Buffer를 Blob으로 변환하여 업로드합니다.
  • 업로드 완료 처리: files.completeUploadExternal API를 호출하여 업로드를 완료합니다. 여기서 channels 속성을 제거했습니다.
  • 채널에 파일 공유: 업로드된 파일을 chat.postMessage API를 사용하여 채널에 공유합니다. 이 부분이 이전에 빠져있던 중요한 단계입니다. file_ids 배열에 업로드된 파일의 ID를 포함시켜 채널에 해당 파일을 첨부합니다.

각 메소드가 무엇을 하는건지 열심히 분석했지만 파일은 업로드가 안된다.

링크를 버튼으로 다운받게 하면 302 Found 에러가 발생한다.

보통 302에러는 사용자가 인증되지 않은 페이지에 엑세스하려고 할때 발생한다고 함.

다운로드 링크와 공개링크 둘다 안됨...

공개 링크의 경우 "files.sharedPublicURL"(파일을 공개/외부 공유가 가능하도록 함)를 통해 만들었는데도 안됨!

 

# 그래서 어떻게 해결했을까?

- API로 GET하고 POST를 해야한다는게 이해가 안됨

- 예를 들어 온도가 40도를 넘었을때, 변수로 40이라는 숫자가져오고

- "온도 40도가 넘었습니다" 라는 문장을 txt파일로 만들어서 Slack으로 전달하고 싶었음

- 계속 검색하다보니 npm에 slack/web-api 라이브러리 존재

 https://www.npmjs.com/package/@slack/web-api

 

@slack/web-api

Official library for using the Slack Platform's Web API. Latest version: 7.8.0, last published: 3 months ago. Start using @slack/web-api in your project by running `npm i @slack/web-api`. There are 644 other projects in the npm registry using @slack/web-ap

www.npmjs.com

 

- 이걸로 한번 시도해보자!

import { getErrorResponse } from '@/app/utils/helper';
import { config } from '../../../../config';
import { NextRequest } from 'next/server';
import { WebClient } from '@slack/web-api';

/**
 * Slack API를 통한 파일 전송 프로세스
 * 1. WebClient 초기화 (Bot Token 사용)
 * 2. filesUploadV2 메서드로 파일 업로드
 * 3. 업로드된 파일 ID를 사용하여 메시지 전송
 */

type BodyType = {
  location: string;
  message: string;
  fileType?: 'text';
};

export async function POST(req: NextRequest) {
  try {
    // 클라이언트로부터 요청 데이터 파싱
    const { message, location, fileType } = await req.json() as BodyType;
    console.log('[REQUEST] 받은 요청:', { message, location, fileType });

    // Slack WebClient 초기화 - Bot Token 사용
    // Bot Token에는 files:write, chat:write 등의 권한이 필요
    const client = new WebClient(config.slack.botToken);

    if (fileType === 'text') {
      const content = "계약현황";  // 파일에 포함될 텍스트 내용
      const fileName = "state.txt"; // 생성될 파일명

      try {
        // Step 1: 파일 업로드 준비
        console.log('[SLACK] 파일 업로드 시작');
        
        // Step 2: filesUploadV2 메서드 사용하여 파일 업로드
        // - Buffer.from(content): 문자열을 버퍼로 변환 (바이트 단위로 변환)
        // - filename: Slack에 표시될 파일 이름
        // - channels: 파일이 공유될 채널 ID
        // - initial_comment: 파일과 함께 표시될 초기 메시지
        const uploadResponse = await client.filesUploadV2({
          file: Buffer.from(content),          // 텍스트 내용을 버퍼로 변환(중요!)
          filename: fileName,                   // 파일 이름 설정
          title: fileName,                     // Slack에서 표시될 제목
          channels: config.slack.channelId,     // 파일을 공유할 채널
          initial_comment: "새로운 계약현황 파일이 업로드되었습니다."  // 초기 메시지
        });

        console.log('[SLACK] 파일 업로드 응답:', uploadResponse); // 성공했으면 끝!

        // Step 3: 업로드 성공 여부 확인
        if (!uploadResponse.ok) {
          throw new Error('파일 업로드 실패');
        }

        // Step 4: 업로드된 파일 정보로 메시지 전송
        // files 배열의 첫 번째 항목에서 파일 ID를 추출
        const fileId = uploadResponse.files?.[0]?.files;
        if (fileId) {
          // Step 5: chat.postMessage로 파일 관련 메시지 전송
          // - channel: 메시지를 보낼 채널
          // - blocks: 메시지의 구조화된 레이아웃
          // - text: 기본 텍스트 (블록이 표시되지 않을 때 사용)
          const messageResponse = await client.chat.postMessage({
            channel: config.slack.channelId || '',
            text: `텍스트 파일이 업로드되었습니다: ${fileName}`,
            blocks: [
              {
                type: "section",
                text: {
                  type: "mrkdwn",
                  text: `*텍스트 파일이 업로드되었습니다*\n\n• 파일명: ${fileName}\n• 내용: ${content}`
                }
              }
            ],
          });

          console.log('[SLACK] 메시지 전송 결과:', messageResponse);
        }

        // Step 6: 클라이언트에 성공 응답 반환
        return new Response(JSON.stringify({
          ok: true,
          message: '파일이 성공적으로 전송되었습니다.',
          filename: fileName
        }), {
          status: 200,
          headers: { 'Content-Type': 'application/json' }
        });

      } catch (error) {
        // 파일 업로드 과정에서 발생한 에러 처리
        console.error('[ERROR] Slack API 오류:', error);
        throw error;
      }
    }

    // 일반 에러 메시지 전송 (파일 업로드가 아닌 경우)
    const messageResponse = await client.chat.postMessage({
      channel: config.slack.channelId || '',
      text: `Error Report\nLocation: ${location}\nMessage: ${message}`
    });

    return new Response(JSON.stringify({ ok: true }), {
      status: 200,
      headers: { 'Content-Type': 'application/json' }
    });

  } catch (error) {
    console.error('[ERROR]', error);
    return getErrorResponse(
      500, 
      error instanceof Error ? error.message : '오류가 발생했습니다.'
    );
  }
}

 

드디어 다운로드 가능!

 

엑셀 스타일 다운받기

import { getErrorResponse } from '@/app/utils/helper';
import { config } from '../../../../config';
import { NextRequest } from 'next/server';
import { WebClient } from '@slack/web-api';
import XLSX from 'xlsx-js-style';  // xlsx-js-style로 변경

type BodyType = {
  location: string;
  message: string;
  fileType?: 'excel';  // 파일 타입을 엑셀로 변경
};

export async function POST(req: NextRequest) {
  try {
    const { message, location, fileType } = await req.json() as BodyType;
    console.log('[REQUEST] 받은 요청:', { message, location, fileType });

    const client = new WebClient(config.slack.botToken);

    if (fileType === 'excel') {
      // 헤더 데이터 정의 (스타일 포함)
      const headers = [
        {
          v: '계약 ID',
          t: 's',
          s: {
            font: { bold: true, color: { rgb: 'FFFFFF' }, sz: 12 },
            fill: { fgColor: { rgb: '1F497D' } },
            alignment: { horizontal: 'center', vertical: 'center' },
            border: {
              top: { style: 'medium', color: { rgb: '000000' } },
              bottom: { style: 'medium', color: { rgb: '000000' } },
              left: { style: 'thin', color: { rgb: '000000' } },
              right: { style: 'thin', color: { rgb: '000000' } }
            }
          }
        },
        // 나머지 헤더들도 동일한 스타일로 정의
        {v: '데이터센터', t: 's', s: { /* 동일한 헤더 스타일 */ }},
        {v: '서버 타입', t: 's', s: { /* 동일한 헤더 스타일 */ }},
        {v: 'CPU', t: 's', s: { /* 동일한 헤더 스타일 */ }},
        {v: 'RAM', t: 's', s: { /* 동일한 헤더 스타일 */ }},
        {v: '스토리지', t: 's', s: { /* 동일한 헤더 스타일 */ }},
        {v: '계약기간', t: 's', s: { /* 동일한 헤더 스타일 */ }},
        {v: '월 비용', t: 's', s: { /* 동일한 헤더 스타일 */ }}
      ];

      // 데이터 행 생성 (스타일 포함)
      const rows = [
        ['DC001', '강남 IDC', '웹 서버', 'Intel Xeon 8코어', '32GB', 'SSD 1TB', 12, 450000],
        ['DC002', '분당 센터', 'DB 서버', 'AMD EPYC 16코어', '64GB', 'NVMe 2TB', 24, 750000],
        ['DC003', '판교 IDC', '백업 서버', 'Intel Xeon 4코어', '16GB', 'HDD 4TB', 6, 300000],
        ['DC004', '광주 센터', '로드밸런서', 'AMD EPYC 8코어', '32GB', 'SSD 500GB', 12, 400000],
        ['DC005', '부산 IDC', '캐시 서버', 'Intel Xeon 12코어', '128GB', 'NVMe 1TB', 24, 600000],
      ].map(row => row.map((cell, index) => {
        const baseStyle = {
          font: { name: 'Arial', sz: 11 },
          alignment: { horizontal: 'center', vertical: 'center' },
          border: {
            top: { style: 'thin', color: { rgb: 'D9D9D9' } },
            bottom: { style: 'thin', color: { rgb: 'D9D9D9' } },
            left: { style: 'thin', color: { rgb: 'D9D9D9' } },
            right: { style: 'thin', color: { rgb: 'D9D9D9' } }
          }
        };

        // 각 컬럼별 특수 스타일 적용
        if (index === 0) {  // 계약 ID
          return {
            v: cell,
            t: 's',
            s: {
              ...baseStyle,
              font: { ...baseStyle.font, color: { rgb: '0066CC' } }
            }
          };
        } else if (index === 6) {  // 계약기간
          return {
            v: cell,
            t: 'n',
            s: {
              ...baseStyle,
              alignment: { horizontal: 'right' },
              numFmt: '0"개월"'
            }
          };
        } else if (index === 7) {  // 월 비용
          return {
            v: cell,
            t: 'n',
            s: {
              ...baseStyle,
              font: { ...baseStyle.font, color: { rgb: 'FF0000' } },
              alignment: { horizontal: 'right' },
              numFmt: '#,##0"원"'
            }
          };
        }
        return { v: cell, t: 's', s: baseStyle };
      }));

      // 워크북 생성
      const wb = XLSX.utils.book_new();
      const ws = XLSX.utils.aoa_to_sheet([headers, ...rows]);

      // 열 너비 설정
      ws['!cols'] = [
        { wch: 12 },  // 계약 ID
        { wch: 15 },  // 데이터센터
        { wch: 12 },  // 서버 타입
        { wch: 20 },  // CPU
        { wch: 10 },  // RAM
        { wch: 12 },  // 스토리지
        { wch: 10 },  // 계약기간
        { wch: 15 }   // 월 비용
      ];

      // 행 높이 설정
      ws['!rows'] = [{ hpt: 30 }];  // 헤더 행 높이

      // 워크시트를 워크북에 추가
      XLSX.utils.book_append_sheet(wb, ws, "계약현황");

      // 엑셀 파일 생성
      const excelBuffer = XLSX.write(wb, {
        type: 'buffer',
        bookType: 'xlsx'
      });

      const fileName = `datacenter_contracts_${new Date().toISOString().split('T')[0]}.xlsx`;

      try {
        console.log('[SLACK] 파일 업로드 시작');
        
        const uploadResponse = await client.filesUploadV2({
          file: excelBuffer,
          filename: fileName,
          title: '데이터센터 계약현황',
          channels: config.slack.channelId,
          initial_comment: "📊 최신 데이터센터 계약현황 보고서가 업로드되었습니다."
        });

        console.log('[SLACK] 파일 업로드 응답:', uploadResponse);

        if (!uploadResponse.ok) {
          throw new Error('파일 업로드 실패');
        }

        // 메시지 전송
        const fileId = uploadResponse.files?.[0]?.files;
        if (fileId) {
          await client.chat.postMessage({
            channel: config.slack.channelId || '',
            blocks: [
              {
                type: "section",
                text: {
                  type: "mrkdwn",
                  text: `*데이터센터 계약현황 보고서*\n\n• 파일명: ${fileName}\n• 업데이트: ${new Date().toLocaleString()}`
                }
              }
            ],
          });
        }

        return new Response(JSON.stringify({
          ok: true,
          message: '엑셀 파일이 성공적으로 전송되었습니다.',
          filename: fileName
        }), {
          status: 200,
          headers: { 'Content-Type': 'application/json' }
        });

      } catch (error) {
        console.error('[ERROR] Slack API 오류:', error);
        throw error;
      }
    }

    return new Response(JSON.stringify({ ok: true }), {
      status: 200,
      headers: { 'Content-Type': 'application/json' }
    });

  } catch (error) {
    console.error('[ERROR]', error);
    return getErrorResponse(
      500, 
      error instanceof Error ? error.message : '오류가 발생했습니다.'
    );
  }
}
728x90

+ Recent posts