구글 스프레드시트에서 중복 값에 스타일 주기 — 서식 > 조건부 서식 > 맞춤 수식에 넣는다.

=COUNTIF(A:A, A:A) > 1
#340

Google Spreadsheet 팝업

function showAlert() {
  const ui = SpreadsheetApp.getUi()
  ui.alert('팝업 창', '표시할 텍스트', ui.ButtonSet.OK)
}

// 메뉴에 추가
function onOpen() {
  SpreadsheetApp.getUi()
    .createMenu('팝업 메뉴')
    .addItem('팝업 띄우기', 'showAlert')
    .addToUi()
}
#337

특수 기호 제거

function removeSpecialCharacters(input) {
  return input.replace(/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]+/g, '')
}

removeSpecialCharacters('Hello_World123! 안녕하세요?')
// "HelloWorld123 안녕하세요"
#336

조건부 렌더링을 한다고 했을때 예전에는 주로 B로 처리했던 것 같은데 디버깅 때문에 고생해서 그런지 생각이 바뀌었다.

function renderA() {
  return <Item isPacked={true} name="Space suit" />
}

function renderB() {
  return <>{isPacked ? <Item name="Space suit" /> : null}</>
}

조건부 렌더링을 보다 간결하게 표현하기 위해 If, Then, 그리고 Else 컴포넌트 개념을 차용하는 방법. 이 컴포넌트는 If에서 condition prop을 통해 조건을 받아들이고, 자식으로 ThenElse를 받아 각각의 내용을 렌더링하도록 한다.

type Props = {
  /** 렌더링할 조건 */
  condition: boolean
  /** 자식 컴포넌트 (Then, Else 포함) */
  children: React.ReactNode
}

/**
 * If 컴포넌트는 조건부 렌더링을 위한 컴포넌트
 */
function If({ condition, children }: Props) {
  let thenChild = null
  let elseChild = null

  React.Children.forEach(children, (child) => {
    if (!React.isValidElement(child)) return
    if (child.type === Then) thenChild = child
    if (child.type === Else) elseChild = child
  })

  return condition ? thenChild : elseChild
}

/**
 * Then, Else 컴포넌트는 조건이 참 또는 거짓일 때 렌더링되는 콘텐츠를 포함
 */
const Then = ({ children }: React.PropsWithChildren) => <>{children}</>
const Else = ({ children }: React.PropsWithChildren) => <>{children}</>
#335

MongoDB TTL 인덱스 (자동 문서 삭제)

// 데이터 삽입
db.collection.insertOne({
  name: 'Temporary',
  expireAt: new Date('2024-11-30T00:00:00Z'),
})

// TTL 인덱스 생성 (expireAt 시간 기준 삭제)
db.collection.createIndex({ expireAt: 1 }, { expireAfterSeconds: 0 })
  • 60초 간격 백그라운드 체크로 삭제 (즉시 아님)
  • expireAt은 ISODate 형식 필수
  • 활용: 세션 만료, 캐시 관리, 임시 데이터

SQLite는 TTL 미지원 → 트리거 또는 외부 스케줄러로 구현

#334

Google Auth ID Token 클레임

클레임 의미
sub 사용자 고유 식별자 (user_id로 사용 가능)
jti JWT 토큰 고유 ID (토큰 무효화/추적용)
iss 토큰 발급자 (accounts.google.com)
aud 대상 애플리케이션 (Client ID)
iat 발급 시각
exp 만료 시각

sub를 user_id로 사용 시 고려사항:

  • Google 계정 삭제 후 재생성 시 새 sub 할당됨
  • 여러 소셜 로그인 지원 시 별도 user_id + google_sub 매핑 권장
#333

SQLite 게시물 업데이트 내역 저장

CREATE TABLE posts (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  title TEXT NOT NULL,
  content TEXT NOT NULL,
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE post_history (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  post_id INTEGER NOT NULL,
  title TEXT,
  content TEXT,
  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (post_id) REFERENCES posts (id) ON DELETE CASCADE
);

-- 업데이트 전 내역 저장
INSERT INTO post_history (post_id, title, content, updated_at)
SELECT id, title, content, updated_at FROM posts WHERE id = 1;

-- 게시물 업데이트
UPDATE posts SET title = '새 제목', updated_at = CURRENT_TIMESTAMP WHERE id = 1;

posts : post_history = 1 : N 관계

#332

SQLite JSON 태그 빈도 정렬

-- 태그별 빈도 계산
SELECT json_each.value AS tag, COUNT(*) AS frequency
FROM example, json_each(example.tags)
GROUP BY json_each.value
ORDER BY frequency DESC, tag ASC;

-- 빈도순 정렬된 태그를 JSON 배열로
SELECT json_group_array(tag) AS tags_by_frequency
FROM (
  SELECT json_each.value AS tag
  FROM example, json_each(example.tags)
  GROUP BY json_each.value
  ORDER BY COUNT(*) DESC, tag ASC
);
#331

빈 PDF 파일이 필요해서 찾아본 방법들

import { createCanvas } from 'skia-canvas'
import { writeFile } from 'node:fs/promises'

/**
 * A4 크기의 빈 PDF 파일을 생성합니다.
 *
 * @param {string} filename - 생성할 PDF 파일의 이름입니다.
 * @returns {Promise<void>}
 */
async function createEmptyPDF(filename) {
  // A4 크기 (595x842 포인트)
  const width = 595
  const height = 842

  // PDF 형식의 캔버스를 생성합니다
  const canvas = createCanvas(width, height, 'pdf')
  const ctx = canvas.getContext('2d')

  // 아무 내용도 그리지 않고 현재 상태를 저장합니다
  ctx.save()

  // 캔버스를 버퍼로 변환하여 PDF로 저장합니다
  const buffer = await canvas.toBuffer()
  await writeFile(filename, buffer)
  console.log(`${filename} 파일이 생성되었습니다!`)
}

createEmptyPDF('empty_skia.pdf')

# Ghostscript를 사용한 빈 PDF 생성
gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=empty.pdf -c "[/PageSize [595 842]] setpagedevice" -f /dev/null

# ImageMagick의 `convert` 명령어로 빈 PDF 생성
convert xc:white -page A4 empty.pdf

# `touch` 명령어와 PDF 헤더 직접 작성
echo -e "%PDF-1.4\n1 0 obj\n<<>>\nendobj\nxref\n0 1\n0000000000 65535 f \ntrailer\n<<>>\nstartxref\n9\n%%EOF" > empty.pdf
#330

Apps Script로 캘린더 예약을 만들 때 쓰는 건 결국 API 두 개다.

Calendar.Freebusy.query로 바쁜 일정을 받아 근무 요일·근무 시간과 겹치는 슬롯을 빼면 예약 가능한 슬롯이 남는다. 예약은 CalendarApp.getCalendarById로 이벤트를 만들고 sendInvites: true로 초대 메일까지 보낸다.

Freebusy는 슬롯을 계산할 때와 실제로 예약할 때 두 번 조회한다 — 그 사이에 일정이 생길 수 있으니.


#329
31 중 14페이지