기본 명령어

curl (Client URL)

  • 의미: 클라이언트 URL 도구
  • 기능: URL을 통해 데이터를 전송하거나 받는 명령어. 웹 페이지 내용 다운로드, API 호출, 파일 다운로드 등 다양한 네트워크 작업에 사용
  • 예시: curl https://example.com - example.com의 HTML 내용을 터미널에 출력
  • 자주 쓰는 옵션:
    • -o - 다운로드한 내용을 파일로 저장
    • -O - 서버의 파일 이름 그대로 저장
    • -X - HTTP 메소드 지정 (GET, POST 등)

vi (Visual Editor)

  • 의미: 비주얼 에디터
  • 기능: 유닉스/리눅스 시스템의 텍스트 편집기. 명령 모드와 입력 모드가 분리되어 있음
  • 주요 명령:
    • q - 종료
    • wq - 저장 후 종료
    • wq! - 강제 저장 후 종료
    • :exit - 종료
    • i - 입력 모드 진입
    • Esc - 명령 모드로 돌아가기
    • dd - 현재 줄 삭제
    • /검색어 - 검색

sudo (Superuser Do)

  • 의미: 슈퍼유저로 실행
  • 기능: 관리자 권한으로 명령어를 실행하는 데 사용. 시스템 파일 수정, 서비스 관리 등에 필요
  • 예시: sudo apt update - 관리자 권한으로 패키지 목록 업데이트
  • 자주 쓰는 옵션:
    • -s - 루트 쉘 실행
    • -u - 다른 사용자 권한으로 명령 실행

cd (Change Directory)

  • 의미: 디렉토리 변경
  • 기능: 작업 디렉토리를 변경하는 명령어
  • 주요 옵션:
    • cd - 홈 디렉토리로 이동
    • cd . - 현재 디렉토리를 의미 (실질적으로는 아무 변화 없음)
    • cd .. - 상위(부모) 디렉토리로 이동
    • cd - - 이전 디렉토리로 이동

pwd (Print Working Directory)

  • 의미: 현재 작업 디렉토리 출력
  • 기능: 현재 위치한 디렉토리의 전체 경로를 표시
  • 예시: pwd → /home/user/documents

tar

  • 의미: Tape Archive의 약자
  • 기능: 여러 파일을 하나의 아카이브 파일로 묶거나 풀 때 사용
  • 자주 쓰는 옵션:
    • c - 아카이브 생성
    • x - 아카이브 압축 해제
    • v - 자세한 정보 출력
    • f - 파일 이름 지정
    • z - gzip 압축/해제
    • j - bzip2 압축/해제

xvfz (tar의 옵션 조합)

  • 의미: 각 문자는 개별 옵션을 의미
    • x - extract (압축 풀기)
    • v - verbose (상세 출력)
    • f - file (파일 지정)
    • z - gzip (gzip 압축 사용)
  • 기능: 주로 tar -xvzf file.tar.gz 형태로 사용되며, gzip으로 압축된 tar 파일의 내용을 상세히 보여주며 압축 해제
  • 예시: tar -xvzf archive.tar.gz - archive.tar.gz 파일의 압축을 풀고 과정을 상세히 보여줌

cp (Copy)

  • 의미: 복사
  • 기능: 파일이나 디렉토리를 복사
  • 예시: cp file.txt backup/ - file.txt를 backup 디렉토리로 복사
  • 자주 쓰는 옵션:
    • -r - 디렉토리 재귀적 복사
    • -i - 덮어쓰기 전 확인
    • -p - 원본 파일의 속성 유지

chmod (Change Mode)

  • 의미: 권한 변경
  • 기능: 파일이나 디렉토리의 접근 권한을 변경
  • 예시: chmod +x script.sh - script.sh 파일에 실행 권한 추가
  • 자주 쓰는 옵션:
    • +x - 실행 권한 추가
    • -w - 쓰기 권한 제거
    • -R - 하위 디렉토리와 파일에 재귀적 적용
    • 755 - 소유자(rwx), 그룹(r-x), 기타(r-x) 권한 설정
    • 644 - 소유자(rw-), 그룹(r--), 기타(r--) 권한 설정

ls (List)

  • 의미: 목록 표시
  • 기능: 디렉토리 내용을 나열
  • 주요 옵션:
    • -a - 숨김 파일 포함 모든 파일 표시
    • -l - 상세 정보 표시
    • -h - 사람이 읽기 쉬운 형식으로 파일 크기 표시 (KB, MB 등)
    • -t - 수정 시간 순으로 정렬
    • -r - 역순 정렬
    • -al | grep ^d - 모든 파일의 상세 정보 중 'D'로 시작하는 항목(디렉토리) 필터링

rpm (Red Hat Package Manager)

  • 의미: 레드햇 패키지 관리자
  • 기능: Red Hat 계열 리눅스에서 소프트웨어 패키지 설치, 업데이트, 제거에 사용
  • 예시: rpm -i package.rpm - package.rpm 설치
  • 자주 쓰는 옵션:
    • -i - 패키지 설치
    • -e - 패키지 제거
    • -q - 패키지 쿼리
    • -U - 패키지 업데이트

mkdir (Make Directory)

  • 의미: 디렉토리 생성
  • 기능: 새 디렉토리를 만드는 명령어
  • 주요 옵션:
    • -p - 필요한 경우 상위 디렉토리까지 함께 생성
  • 예시: mkdir -p a/b/c - a, b, c 디렉토리를 계층적으로 한 번에 생성

mv (Move)

  • 의미: 이동
  • 기능: 파일이나 디렉토리를 이동하거나 이름 변경
  • 예시: mv file.txt newname.txt - file.txt의 이름을 newname.txt로 변경
  • 자주 쓰는 옵션:
    • -i - 덮어쓰기 전 확인
    • -f - 강제 이동 (확인 없음)
    • -v - 작업 과정 표시

ln (Link)

  • 의미: 링크 생성
  • 기능: 파일이나 디렉토리에 대한 하드 링크나 심볼릭 링크를 생성
  • 예시: ln -s target link_name - target에 대한 심볼릭 링크 link_name 생성
  • 자주 쓰는 옵션:
    • -s - 심볼릭 링크 생성
    • -f - 이미 존재하는 링크 파일 덮어쓰기

chown (Change Owner)

  • 의미: 소유자 변경
  • 기능: 파일이나 디렉토리의 소유자나 그룹을 변경
  • 주요 옵션:
    • -R - Recursive, 디렉토리 내의 모든 파일과 하위 디렉토리에 재귀적으로 적용
  • 예시: chown -R user:group directory/ - directory와 그 내부 모든 항목의 소유자를 user와 group으로 변경

make

  • 의미: 메이크 도구 실행
  • 기능: Makefile에 정의된 규칙에 따라 프로그램을 컴파일하거나 다른 작업 수행
  • 주요 옵션:
    • clean - 이전 빌드 파일 제거 (Makefile에 정의된 경우)
    • all - 모든 타겟 빌드 (Makefile에 정의된 경우)
  • 예시: make clean all - 먼저 이전 빌드 파일을 제거하고 전체 프로젝트를 다시 빌드

ls -lrt

  • 의미: 파일 목록을 상세히, 역순으로, 수정 시간 기준으로 정렬하여 표시
  • 기능: -l (상세 정보), -r (역순), -t (시간순) 옵션의 조합
  • 예시: ls -lrt - 가장 최근에 수정된 파일이 맨 아래에 표시

gcc -o

  • 의미: GNU C 컴파일러 출력 파일 지정
  • 기능: C 소스 코드를 컴파일하고 실행 파일 이름을 지정
  • 예시: gcc -o program source.c - source.c 파일을 컴파일하여 'program'이라는 이름의 실행 파일 생성
  • 자주 쓰는 옵션:
    • -o - 출력 파일 이름 지정
    • -c - 컴파일만 하고 링크는 하지 않음
    • -g - 디버깅 정보 포함
    • -Wall - 모든 경고 메시지 표시

추가 자주 쓰이는 명령어

grep (Global Regular Expression Print)

  • 의미: 정규 표현식 검색
  • 기능: 파일이나 출력에서 패턴을 검색
  • 예시: grep "error" log.txt - log.txt 파일에서 "error" 문자열 찾기
  • 자주 쓰는 옵션:
    • -i - 대소문자 구분 없이 검색
    • -r - 디렉토리 내 모든 파일에서 재귀적으로 검색
    • -n - 행 번호 표시
    • -v - 패턴을 포함하지 않는 행 표시

find

  • 의미: 파일 검색
  • 기능: 파일 시스템에서 조건에 맞는 파일이나 디렉토리 검색
  • 예시: find /home -name "*.txt" - /home 디렉토리 및 하위 디렉토리에서 .txt 파일 모두 찾기
  • 자주 쓰는 옵션:
    • -name - 파일 이름으로 검색
    • -type - 파일 타입으로 검색 (f: 일반 파일, d: 디렉토리)
    • -mtime - 수정 시간으로 검색
    • -exec - 찾은 파일에 명령 실행

ps (Process Status)

  • 의미: 프로세스 상태
  • 기능: 실행 중인 프로세스 목록 표시
  • 예시: ps aux - 시스템의 모든 프로세스 상세 정보 표시
  • 자주 쓰는 옵션:
    • aux - 모든 프로세스에 대한 자세한 정보
    • ef - 모든 프로세스를 트리 구조로 표시

top

  • 의미: 테이블 오브 프로세스 (Table of Processes)
  • 기능: 실시간으로 시스템 리소스 사용 및 프로세스 정보 표시
  • 주요 키:
    • q - 종료
    • k - 프로세스 종료 (kill)
    • r - 프로세스 우선순위 변경
    • 1 - CPU 코어별 사용률 표시

cat (Concatenate)

  • 의미: 연결
  • 기능: 파일 내용을 화면에 출력하거나 여러 파일을 연결
  • 예시: cat file.txt - file.txt의 내용을 화면에 출력
  • 자주 쓰는 옵션:
    • -n - 행 번호 표시
    • -b - 비어있지 않은 행에만 번호 표시

less

  • 의미: 페이지 단위로 표시
  • 기능: 파일 내용을 페이지 단위로 보여주는 텍스트 뷰어
  • 예시: less large_file.log - large_file.log를 페이지 단위로 탐색
  • 주요 키:
    • q - 종료
    • /pattern - 패턴 검색
    • n - 다음 검색 결과로 이동
    • space - 다음 페이지
    • b - 이전 페이지

head / tail

  • 의미: 파일의 시작/끝 부분 표시
  • 기능:
    • head - 파일의 처음 몇 줄 표시
    • tail - 파일의 마지막 몇 줄 표시
  • 예시:
    • head -n 10 file.txt - file.txt의 처음 10줄 표시
    • tail -f log.txt - log.txt 파일의 마지막 부분을 실시간으로 계속 표시
  • 자주 쓰는 옵션:
    • -n - 표시할 줄 수 지정
    • -f (tail) - 파일에 추가되는 내용 실시간 표시 (로그 모니터링에 유용)

df (Disk Free)

  • 의미: 디스크 여유 공간
  • 기능: 파일 시스템의 디스크 공간 사용량 표시
  • 예시: df -h - 사람이 읽기 쉬운 형식으로 디스크 사용량 표시
  • 자주 쓰는 옵션:
    • -h - 사람이 읽기 쉬운 형식으로 표시 (GB, MB 등)
    • -T - 파일 시스템 유형 표시

du (Disk Usage)

  • 의미: 디스크 사용량
  • 기능: 디렉토리와 파일의 디스크 사용량 표시
  • 예시: du -sh * - 현재 디렉토리의 모든 파일과 디렉토리 크기를 요약해서 표시
  • 자주 쓰는 옵션:
    • -s - 요약 정보만 표시
    • -h - 사람이 읽기 쉬운 형식으로 표시
    • -a - 모든 파일 표시 (디렉토리만이 아닌)

ssh (Secure Shell)

  • 의미: 보안 쉘
  • 기능: 원격 호스트에 안전하게 접속
  • 예시: ssh user@hostname - 원격 호스트에 사용자 계정으로 접속
  • 자주 쓰는 옵션:
    • -p - 포트 번호 지정
    • -i - 개인 키 파일 지정

scp (Secure Copy)

  • 의미: 보안 복사
  • 기능: SSH를 통해 원격 호스트와 파일을 안전하게 복사
  • 예시:
    • scp file.txt user@remote:/path/ - 로컬 파일을 원격 호스트로 복사
    • scp user@remote:/path/file.txt local/ - 원격 파일을 로컬로 복사
  • 자주 쓰는 옵션:
    • -r - 디렉토리 재귀적 복사
    • -P - 포트 번호 지정

ping

  • 의미: 패킷 인터넷 그로퍼 (Packet Internet Groper)
  • 기능: 네트워크 연결 테스트
  • 예시: ping google.com - google.com과의 네트워크 연결 상태 확인
  • 자주 쓰는 옵션:
    • -c - 지정한 횟수만큼만 패킷 전송
    • -t (Windows) / -c 0 (Linux) - 중단할 때까지 계속 ping

kill

  • 의미: 프로세스 종료
  • 기능: 프로세스 ID(PID)를 이용해 프로세스 종료
  • 예시: kill 1234 - PID 1234 프로세스 종료
  • 자주 쓰는 옵션:
    • -9 - 강제 종료 시그널 (SIGKILL)
    • -15 - 정상 종료 시그널 (SIGTERM, 기본값)

wget

  • 의미: 웹 겟 (Web Get)
  • 기능: 웹에서 파일 다운로드
  • 예시: wget https://example.com/file.zip - URL에서 파일 다운로드
  • 자주 쓰는 옵션:
    • -O - 다운로드 파일 이름 지정
    • -c - 이어받기
    • -r - 재귀적 다운로드 (웹사이트 미러링)

apt / yum / dnf

  • 의미: 패키지 관리자
  • 기능: 소프트웨어 패키지 설치, 업데이트, 제거
    • apt - Debian/Ubuntu 계열
    • yum/dnf - Red Hat/Fedora 계열
  • 예시:
    • apt update && apt upgrade - 패키지 목록 업데이트 후 모든 패키지 업그레이드
    • yum install package-name - 패키지 설치
  • 주요 명령:
    • install - 패키지 설치
    • remove - 패키지 제거
    • update/upgrade - 패키지 업데이트
    • search - 패키지 검색

history

  • 의미: 명령어 기록
  • 기능: 이전에 사용한 명령어 목록 표시
  • 예시: history | grep cd - cd 명령어 사용 기록 검색
  • : !숫자 - history에서 해당 번호의 명령어 재실행
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

쿼리파라미터와 동적라우팅 URL 방식의 차이

  1. 쿼리 파라미터 방식
    • URL에 ? 이후로 키-값 쌍으로 데이터를 전달합니다.
    • API 라우트에서는 request.nextUrl.searchParams로 값을 추출합니다.
    • 라우트 파일 위치: /app/api/sensor/route.ts
    • URL: `/api/sensor?sensorId=${sensorCodeId}`
  2. 동적 라우팅 방식
    • URL 경로의 일부로 값을 전달합니다.
    • API 라우트에서는 params 객체로 값을 추출합니다.
    • 라우트 파일 위치: /app/api/sensor/[sensorCodeId]/route.ts
    • URL: `/api/sensor/${sensorCodeId}`

쿼리 파라미터가 적합한 경우

  • 선택적(optional) 파라미터가 여러 개일 때
  • 필터링, 정렬, 검색 등 부가적인 옵션을 전달할 때
  • 페이지네이션(예: ?page=2&limit=10)
  • URL을 깔끔하게 유지하고 싶을 때
  • 같은 엔드포인트에 여러 파라미터가 조합될 때

동적 라우팅(/path/[param])이 적합한 경우:

  • 리소스의 고유 식별자를 표현할 때(ID, 슬러그 등)
  • RESTful API 설계를 따를 때
  • 경로가 리소스의 계층 구조를 표현할 때
  • URL이 의미적으로 중요할 때 (SEO에 유리)
  • 필수적인(required) 파라미터일 때

 

현재 상황(ID를 받아서 검색하는 경우)에 대한 추천

동적 라우팅 방식이 더 적합합니다.

이유

  1. 특정 센서 코드라는 리소스에 직접 접근하는 패턴입니다.
  2. RESTful API 설계 원칙에 더 부합합니다.
  3. sensorCodeId는 필수 파라미터이며 리소스의 식별자입니다.
  4. URL이 더 의미적이고 명확해집니다.
728x90

시계열 데이터베이스(TSDB) 개요

시계열 데이터베이스(Time Series Database, TSDB)는 시간에 따라 변화하는 데이터를 효율적으로 저장하고 처리하기 위해 설계된 특수 데이터베이스입니다.

개념

시계열 데이터베이스는 시간 순서대로 정렬된 데이터 포인트를 저장하는 데 최적화되어 있습니다. 각 데이터 포인트는 타임스탬프와 연관된 값으로 구성됩니다. 예를 들어, 주식 가격, 센서 측정값, 서버 모니터링 로그 등이 시계열 데이터에 해당합니다.

중요성

  • IoT와 모니터링의 증가: 점점 더 많은 기기와 시스템이 지속적으로 데이터를 생성하고 있습니다.
  • 실시간 분석: 빠르게 변화하는 데이터의 추세와 패턴을 실시간으로 파악해야 합니다.
  • 대용량 데이터 처리: 기존 데이터베이스로는 처리하기 어려운 대량의 시계열 데이터를 효율적으로 관리합니다.
  • 예측 분석: 과거 데이터를 기반으로 미래 동향을 예측하는 데 필수적입니다.

특징

  • 시간 기반 인덱싱: 타임스탬프를 기준으로 빠른 검색이 가능합니다.
  • 높은 쓰기 처리량: 초당 수천/수백만 데이터 포인트를 기록할 수 있습니다.
  • 데이터 압축: 공간 효율성을 위해 시계열 특성을 활용한 압축 기술을 사용합니다.
  • 데이터 보존 정책: 오래된 데이터는 자동으로 다운샘플링하거나 삭제할 수 있습니다.
  • 시계열 특화 쿼리 언어: 시간 기반 집계, 보간, 다운샘플링 등의 기능을 제공합니다.

RDBMS 및 NoSQL과의 차이점

RDBMS와의 차이

  • 데이터 모델: RDBMS는 관계형 모델을 사용하지만, TSDB는 시간 기반 모델을 사용합니다.
  • 성능: RDBMS는 시계열 데이터의 대용량 쓰기 작업에 비효율적입니다.
  • 쿼리 최적화: TSDB는 시간 범위 쿼리에 최적화되어 있습니다.
  • 스키마: RDBMS는 엄격한 스키마를 요구하지만, 많은 TSDB는 유연한 스키마를 제공합니다.

NoSQL과의 차이

  • 목적: NoSQL은 다양한 비구조적 데이터에 적합하지만, TSDB는 시계열 데이터에 특화되어 있습니다.
  • 쿼리 기능: TSDB는 시간 범위 분석, 집계, 보간 등 시계열 특화 기능이 풍부합니다.
  • 데이터 처리: TSDB는 시간 순서 데이터의 효율적인 압축과 보존 정책을 제공합니다.

대표적인 시계열 데이터베이스

  1. InfluxDB: 가장 인기 있는 오픈 소스 TSDB로, 자체 쿼리 언어인 InfluxQL과 Flux를 제공합니다.
  2. Prometheus: 모니터링에 특화된 TSDB로, 알림과 시각화 기능이 강력합니다.
  3. TimescaleDB: PostgreSQL 확장으로, SQL의 친숙함과 TSDB의 성능을 결합했습니다.
  4. Graphite: 메트릭 데이터를 저장하고 그래프로 표시하는 데 특화된 시스템입니다.
  5. OpenTSDB: HBase 위에 구축된 확장성이 뛰어난 TSDB입니다.
  6. Kdb+/q: 금융 분야에서 널리 사용되는 고성능 상용 TSDB입니다.
  7. Amazon Timestream: AWS의 서버리스 시계열 데이터베이스 서비스입니다.

이러한 데이터베이스들은 기존 RDBMS의 HeidiSQL과 같은 범용 관리 도구와는 달리, 각각 고유한 관리 및 쿼리 인터페이스를 제공합니다. 예를 들어, InfluxDB는 Chronograf, Prometheus는 Grafana와 같은 시각화 도구와 연동되어 사용됩니다.

728x90

'코딩공부 > DB' 카테고리의 다른 글

mysql 데이터베이스 기본 명령어  (0) 2022.12.06
★현재까지 배운 DB관련 내용 총정리★  (0) 2022.12.05

useState vs useSWR vs Redux

1. useState

  • 장점
    • 간단한 로컬 상태 관리
    • 별도 설정 없이 사용 가능
    • 컴포넌트 내부 상태 관리에 최적
  • 단점
    • 복잡한 상태 관리에 부적합
    • 상태 공유가 어려움
    • 전역 상태 관리 불가

2. useSWR

  • 특징
    • 데이터 페칭과 캐싱에 특화된 훅
    • 실시간 데이터 동기화
    • 자동 재검증 및 캐시 관리
  • 장점
    • 간편한 데이터 로딩 상태 관리
    • 캐시 및 재검증 기능
    • 성능 최적화
  • 단점
    • 주로 원격 데이터 페칭에 특화
    • 복잡한 상태 관리에는 부적합

3. Redux

  • 장점
    • 복잡한 전역 상태 관리
    • 상태 변경의 예측 가능성
    • 디버깅 용이
    • 미들웨어 지원
  • 단점
    • 보일러플레이트 코드 많음
    • 초기 설정 복잡
    • 상대적으로 학습 난이도 높음

사용 추천 상황

  1. useState:
    • 간단한 컴포넌트 내 상태 관리
    • 소규모 프로젝트
  2. useSWR:
    • 데이터 페칭이 주요 관심사
    • 실시간 데이터 동기화 필요
    • API 요청 최적화
  3. Redux:
    • 대규모 애플리케이션
    • 복잡한 상태 관리
    • 상태 추적과 디버깅 필요
    • 여러 컴포넌트 간 상태 공유

결론

상황과 요구사항에 따라 적절한 상태 관리 방식을 선택하는 것이 중요합니다. 각 방식은 고유의 장단점을 가지고 있어 프로젝트의 특성을 고려해야 합니다.

728x90

Next.js와 HeidiSQL을 사용하여 파일 첨부 및 데이터베이스 저장하는 방법

파일 저장 접근 방법

파일을 데이터베이스에 저장하는 방법은 주로 두 가지가 있습니다

1. BLOB 데이터 타입 사용
2. 파일 경로 저장 및 실제 파일은 서버에 저장

BLOB 데이터 타입을 사용한 직접 저장 방법
1. 데이터베이스 테이블 구조 설정


CREATE TABLE uploaded_files (
    id INT AUTO_INCREMENT PRIMARY KEY,
    filename VARCHAR(255),
    file_data LONGBLOB,
    file_type VARCHAR(100),
    uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);




2. Next.js API 라우트 구현

import { NextApiRequest, NextApiResponse } from 'next';
import mysql from 'mysql2/promise';

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
    if (req.method === 'POST') {
        try {
            // 데이터베이스 연결 설정
            const connection = await mysql.createConnection({
                host: 'localhost',
                user: 'your_username',
                password: 'your_password',
                database: 'your_database'
            });

            // 파일 데이터 추출 (multipart/form-data 사용 가정)
            const { originalFilename, mimetype, buffer } = req.body;

            // 파일을 데이터베이스에 저장하는 쿼리
            const [result] = await connection.execute(
                'INSERT INTO uploaded_files (filename, file_data, file_type) VALUES (?, ?, ?)',
                [originalFilename, buffer, mimetype]
            );

            await connection.end();

            res.status(200).json({ message: '파일 업로드 성공', fileId: result.insertId });
        } catch (error) {
            res.status(500).json({ message: '파일 업로드 실패', error: error.message });
        }
    } else {
        res.status(405).json({ message: '허용되지 않은 메서드' });
    }
}


주의사항

1. 파일 크기 제한: BLOB 타입은 큰 파일을 저장할 수 있지만, 성능과 데이터베이스 용량에 영향을 줄 수 있습니다.

2.보안  
   - 파일 업로드 시 파일 유형, 크기 제한
   - 악성 파일 검사
   - 인증/권한 확인

3. 대안적 접근
   - 파일은 서버나 클라우드 스토리지에 저장
   - 데이터베이스에는 파일 경로만 저장

파일 업로드 프론트엔드 예시

import { useState } from 'react';
import axios from 'axios';

export default function FileUpload() {
    const [selectedFile, setSelectedFile] = useState<File | null>(null);

    const handleFileUpload = async () => {
        if (!selectedFile) return;

        const formData = new FormData();
        formData.append('file', selectedFile);

        try {
            const response = await axios.post('/api/upload', formData, {
                headers: { 'Content-Type': 'multipart/form-data' }
            });
            console.log('파일 업로드 성공:', response.data);
        } catch (error) {
            console.error('파일 업로드 실패:', error);
        }
    };

    return (
        <div>
            <input 
                type="file" 
                onChange={(e) => setSelectedFile(e.target.files?.[0] || null)}
            />
            <button onClick={handleFileUpload}>업로드</button>
        </div>
    );
}




추천 패키지
- 파일 업로드: `multer`, `formidable`
- 데이터베이스 연결: `mysql2`

이 방법은 파일을 직접 MySQL 데이터베이스에 BLOB 형태로 저장하는 방법입니다. 대용량 파일이나 많은 파일을 다룰 경우 파일 시스템이나 클라우드 스토리지를 고려하는 것이 좋습니다.

728x90

 

서버 렌더링의 이점

  • 데이터 가져오기: 데이터를 서버에서 직접 가져오면 더 빨리 가져올 수 있고, 클라이언트에서 요청할 필요가 줄어들어 성능이 좋아짐.
  • 보안: 중요한 정보나 로직을 서버에만 두면 클라이언트에게 노출되지 않아 보안이 강화됨.
  • 캐싱: 서버에서 렌더링한 결과를 저장해서 나중에 재사용하면, 매번 새로 렌더링할 필요가 없어서 성능이 좋아지고 비용도 절감됨.
  • 성능: 인터랙티브하지 않은 부분을 서버에서 렌더링하면 클라이언트에서 처리할 JavaScript가 줄어들어서, 느린 인터넷이나 성능이 낮은 기기에서도 더 나은 경험을 제공할 수 있음.
  • 초기 페이지 로드: 서버에서 HTML을 미리 생성하면, 사용자가 페이지를 더 빨리 볼 수 있음.
  • 검색 엔진 최적화: 서버에서 렌더링한 HTML을 검색 엔진이 인덱싱하기 쉽고, 소셜 네트워크에서 미리보기를 잘 생성함.
  • 스트리밍: 렌더링 작업을 나누어서 준비된 부분부터 클라이언트로 보내면, 사용자가 페이지의 일부를 더 빨리 볼 수 있음.

 

728x90

+ Recent posts