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 미지원 → 트리거 또는 외부 스케줄러로 구현
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 매핑 권장
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 관계
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
); 빈 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 Google Apps Script 캘린더 예약 시스템 - 시간 슬롯 조회 및 예약 구현
fetchAvailability - 지정된 조건(근무 요일, 시간, 이벤트 충돌)에 따라 예약 가능한 시간 슬롯 조회
flowchart LR
A[시작] --> B[가장 가까운 슬롯 계산]
B --> C[예약 기간 설정]
C --> D[바쁜 일정 조회]
D --> E[가능한 슬롯 필터링]
E --> F[예약 가능한 슬롯 반환]
F --> G[종료]
- 현재 시간을 기준으로 가장 가까운 시간 슬롯을 계산합니다. (슬롯 길이는
TIMESLOT_DURATION에 따라 설정됩니다.) - 28일(
DAYS_IN_ADVANCE) 동안의 일정 기간을 설정합니다. Calendar.Freebusy.query를 사용하여 지정된 캘린더(CALENDAR)의 바쁜 일정(busy events)을 조회합니다.- 조회된 이벤트를 기반으로 조건에 맞지 않는 시간 슬롯을 제외합니다:
- 지정된 근무 시간(
WORKHOURS.start,WORKHOURS.end) 외의 시간. - 근무일(
WORKDAYS)이 아닌 요일. - 다른 이벤트와 시간이 겹치는 경우.
- 지정된 근무 시간(
- 예약 가능한 시간 슬롯을 ISO 8601 형식의 문자열로 저장하고 반환합니다.
bookTimeslot - 선택한 시간 슬롯에 이벤트 생성. 성공 시 Calendar에 추가, 확인 메시지 반환.
flowchart TD
A[시작] --> B[사용자 입력 수신]
B --> C[시간 슬롯 유효성 검증]
C --> D[이벤트 충돌 확인]
D --> E{이벤트가 존재합니까?}
E -- 예 --> F[에러 반환]
E -- 아니오 --> G[이벤트 생성]
G --> H[성공 메시지 반환]
F --> I[종료]
H --> I
- 사용자가 선택한 시간 슬롯(
timeslot)과 추가 정보(이름, 이메일, 전화번호, 메모)를 인수로 받습니다. - 시간 슬롯의 유효성을 검증하고,
TIMESLOT_DURATION을 기준으로 종료 시간을 계산합니다. Calendar.Freebusy.query를 통해 선택한 시간 동안 다른 이벤트가 있는지 확인합니다.- 이벤트가 겹치면 예약이 불가능하다는 에러 메시지를 반환합니다.
- 겹치는 이벤트가 없다면,
CalendarApp.getCalendarById를 사용하여 Google Calendar에 새로운 이벤트를 생성합니다:- 이벤트 제목: 사용자 이름이 포함된 약속 제목.
- 이벤트 설명: 전화번호와 메모를 포함합니다.
- 초대된 손님: 제공된 이메일로 초대합니다.
- 초대 메일 발송(
sendInvites: true).
- 예약 성공 여부를 메시지로 반환합니다.
사용 플로우: 시간 조회 → 시간 선택 → bookTimeslot → 이벤트 생성 → 초대 이메일 발송
AuthContext 테스트 패턴
// localStorage 함수 테스트
vi.spyOn(Storage.prototype, 'getItem').mockImplementation((key) => {
if (key === 'auth_token') return 'test_token'
return null
})
// AuthProvider 상태 테스트
const TestComponent = () => {
const { token, login, logout } = useAuth()
return (
<div>
<p data-testid="token">{token || 'null'}</p>
<button onClick={() => login('test_token')}>Login</button>
<button onClick={logout}>Logout</button>
</div>
)
}
// useAuth 훅 테스트
it('AuthProvider 외부에서 호출하면 에러', () => {
expect(() => renderHook(() => useAuth())).toThrowError()
})
it('AuthProvider 내부에서 정상 동작', () => {
const wrapper = ({ children }) => <AuthProvider>{children}</AuthProvider>
const { result } = renderHook(() => useAuth(), { wrapper })
expect(result.current).toHaveProperty('token', null)
}) 타이머를 제어하기 위해 vi.useFakeTimers와 vi.advanceTimersByTime을 사용하기.
describe('Countdown 컴포넌트', () => {
beforeEach(() => {
vi.useFakeTimers()
})
afterEach(() => {
vi.useRealTimers()
})
test('타이머가 동작하며 시간을 업데이트한다.', () => {
const endTime = new Date(Date.now() + 60000).toISOString()
const renderChild = vi.fn(({ targetRef }) => (
<div ref={targetRef} data-testid="countdown" />
))
render(<Countdown end_at={endTime}>{renderChild}</Countdown>)
expect(screen.getByTestId('countdown')).toBeEmptyDOMElement()
act(() => {
vi.advanceTimersByTime(10000)
})
expect(screen.getByTestId('countdown')).toHaveTextContent('00:00:50')
act(() => {
vi.advanceTimersByTime(50000)
})
expect(screen.getByTestId('countdown')).toHaveTextContent('00:00:00')
})
})
폼 제출 시 FormData 테스트
test('폼 제출 시 formData 확인', () => {
const handleSubmit = jest.fn((e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
const formData = new FormData(e.target as HTMLFormElement)
const data = Object.fromEntries(formData.entries())
expect(data).toEqual({ username: 'testuser', password: 'password' })
})
render(<LoginForm onSubmit={handleSubmit} />)
fireEvent.change(screen.getByPlaceholderText('아이디'), {
target: { value: 'testuser' },
})
fireEvent.change(screen.getByPlaceholderText('비밀번호'), {
target: { value: 'password' },
})
fireEvent.click(screen.getByRole('button', { name: '로그인' }))
expect(handleSubmit).toHaveBeenCalled()
}) 거리 만km 포맷팅
function formatToManKm(distance: number): string {
const manKm = (distance / 10000).toFixed(1)
return `${manKm}만km`
}
formatToManKm(104335) // "10.4만km"