콘텐츠로 이동

Timer API 가이드 (프론트엔드 개발자용)

최종 업데이트: 2026-01-28 중요 변경: 타이머 제어 작업이 WebSocket 기반으로 전환되었습니다.

개요

Timer API는 일정(Schedule), 할 일(Todo), 또는 독립적으로 타이머를 생성하고 관리할 수 있습니다.

핵심 개념

개념 설명
Timer 시간 측정 세션. Schedule, Todo, 또는 둘 다에 연결 가능. 독립 타이머도 가능.
Schedule 캘린더 일정. 타이머를 통해 작업 시간 측정 가능.
Todo 할 일 항목. 타이머를 통해 작업 시간 측정 가능.

아키텍처 변경 (2026-01-28)

변경 이유

  1. 일시정지 이력 추적: 단일 paused_at 컬럼으로는 여러 번의 일시정지/재개 이력 저장 불가
  2. 멀티 플랫폼 동기화: REST 폴링 방식으로는 실시간 동기화 어려움
  3. 친구 알림: 친구의 타이머 활동을 실시간으로 알림

새로운 아키텍처

flowchart TD
    subgraph clients["클라이언트"]
        Web[Web]
        Mobile[Mobile]
        Desktop[Desktop]
    end

    WS[/"WebSocket /v1/ws/timers"/]

    subgraph sync["실시간 동기화"]
        MyDevice[내 기기 동기화]
        OtherDevice[다른 기기 동기화]
        FriendNotify[친구 알림]
    end

    Web --> WS
    Mobile --> WS
    Desktop --> WS
    WS --> MyDevice
    WS --> OtherDevice
    WS --> FriendNotify

API 변경 요약

작업 이전 (REST) 현재 (WebSocket)
타이머 생성 POST /v1/timers timer.create 메시지
일시정지 PATCH /v1/timers/{id}/pause timer.pause 메시지
재개 PATCH /v1/timers/{id}/resume timer.resume 메시지
종료 POST /v1/timers/{id}/stop timer.stop 메시지
조회 GET /v1/timers/* 유지 (REST)
삭제 DELETE /v1/timers/{id} 유지 (REST)
업데이트 PATCH /v1/timers/{id} 유지 (REST)

데이터 모델

Timer

interface Timer {
  id: string;                   // UUID
  schedule_id?: string;         // Schedule ID (Optional)
  todo_id?: string;             // Todo ID (Optional)
  title?: string;               // 타이머 제목
  description?: string;         // 타이머 설명
  allocated_duration: number;   // 할당 시간 (초 단위)
  elapsed_time: number;         // 경과 시간 (초 단위)
  status: TimerStatus;          // 상태
  started_at?: string;          // 시작 시간 (ISO 8601)
  paused_at?: string;           // 마지막 일시정지 시간 (ISO 8601)
  ended_at?: string;            // 종료 시간 (ISO 8601)
  pause_history: PauseEvent[];  // 일시정지/재개 이력 (NEW!)
  created_at: string;           // 생성 시간 (ISO 8601)
  updated_at: string;           // 수정 시간 (ISO 8601)
  schedule?: Schedule;          // Schedule 정보
  todo?: Todo;                  // Todo 정보
  tags: Tag[];                  // 연결된 태그 목록
  owner_id?: string;            // 소유자 ID
  is_shared: boolean;           // 공유된 타이머인지
}

type TimerStatus = 
  | "RUNNING"    // 실행 중
  | "PAUSED"     // 일시정지
  | "COMPLETED"  // 완료
  | "CANCELLED"; // 취소됨

interface PauseEvent {
  action: "start" | "pause" | "resume" | "stop" | "cancel";
  at: string;           // ISO 8601 시간
  elapsed?: number;     // 경과 시간 (pause, stop 시)
}

pause_history 예시

[
  { "action": "start", "at": "2026-01-28T10:00:00Z" },
  { "action": "pause", "at": "2026-01-28T10:30:00Z", "elapsed": 1800 },
  { "action": "resume", "at": "2026-01-28T10:35:00Z" },
  { "action": "pause", "at": "2026-01-28T10:50:00Z", "elapsed": 2700 },
  { "action": "resume", "at": "2026-01-28T11:00:00Z" },
  { "action": "stop", "at": "2026-01-28T11:30:00Z", "elapsed": 4500 }
]

WebSocket API

연결

중요: 보안상 JWT는 반드시 Sec-WebSocket-Protocol 헤더로 전달해야 합니다.

개발 환경:

const ws = new WebSocket(
  'ws://localhost:8000/v1/ws/timers?timezone=Asia/Seoul',
  ['authorization.bearer.' + jwtToken]
);

프로덕션 환경:

const ws = new WebSocket(
  'wss://your-domain.com/v1/ws/timers?timezone=Asia/Seoul',
  ['authorization.bearer.' + jwtToken]
);

쿼리 파라미터: - timezone: 타임존 (선택, 예: UTC, +09:00, Asia/Seoul) - 지정하지 않으면 UTC naive datetime으로 반환 - 지정하면 모든 응답의 datetime 필드가 해당 타임존으로 변환됨

인증: - Sec-WebSocket-Protocol 헤더: authorization.bearer.<JWT_TOKEN> (필수) - WebSocket 생성자의 두 번째 인자(subprotocols)로 전달

연결 후 자동 동기화 (NEW!) 🔥

연결 즉시 활성 타이머가 자동으로 전송됩니다!

sequenceDiagram
    participant Client
    participant Server
    Client->>Server: WebSocket 연결
    Server->>Client: 1. connected 메시지
    Server->>Client: 2. timer.sync_result (자동 전송)
    Note over Client: 즉시 타이머 상태 동기화 완료!

특징: - ✅ 자동 전송: 연결 즉시 활성 타이머(RUNNING/PAUSED) 자동 수신 - ✅ 빠른 초기화: 별도 sync 요청 불필요 - ✅ 멀티 디바이스: 새 기기 연결 시 즉시 동기화

중요: CORS 설정

WebSocket 연결이 작동하려면 백엔드 서버의 CORS_ALLOWED_ORIGINS 환경변수에 WebSocket URL을 반드시 추가해야 합니다:

CORS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:8000,http://127.0.0.1:3000,http://127.0.0.1:8000,ws://localhost:8000,ws://127.0.0.1:8000
CORS_ALLOWED_ORIGINS=https://example.com,https://app.example.com,wss://api.example.com

ws://는 HTTP용, wss://는 HTTPS용입니다. 프로덕션에서는 반드시 wss://를 사용하세요.

연결 성공 응답

1. 연결 확인 메시지:

{
  "type": "connected",
  "payload": {
    "user_id": "user-uuid",
    "message": "Connected to timer WebSocket"
  },
  "timestamp": "2026-01-28T10:00:00Z"
}

2. 자동 동기화 메시지 (즉시 전송):

{
  "type": "timer.sync_result",
  "payload": {
    "timers": [
      {
        "id": "timer-uuid",
        "title": "작업 중인 타이머",
        "status": "RUNNING",
        "elapsed_time": 1234,
        ...
      }
    ],
    "count": 1
  },
  "from_user": "user-uuid",
  "timestamp": "2026-01-28T10:00:00Z"
}

참고: timers 배열이 비어있으면(count: 0) 활성 타이머가 없는 상태입니다.


클라이언트 → 서버 메시지

타이머 생성 (timer.create)

{
  "type": "timer.create",
  "payload": {
    "allocated_duration": 3600,
    "title": "작업 타이머",
    "description": "프로젝트 작업",
    "schedule_id": "uuid-or-null",
    "todo_id": "uuid-or-null",
    "tag_ids": ["tag-uuid-1"]
  }
}
필드 타입 필수 설명
allocated_duration number 할당 시간 (초 단위, 양수 필수)
title string 타이머 제목
description string 타이머 설명
schedule_id UUID Schedule ID
todo_id UUID Todo ID
tag_ids UUID[] 태그 ID 리스트

타이머 일시정지 (timer.pause)

{
  "type": "timer.pause",
  "payload": {
    "timer_id": "timer-uuid"
  }
}

타이머 재개 (timer.resume)

{
  "type": "timer.resume",
  "payload": {
    "timer_id": "timer-uuid"
  }
}

타이머 종료 (timer.stop)

{
  "type": "timer.stop",
  "payload": {
    "timer_id": "timer-uuid"
  }
}

타이머 동기화 요청 (timer.sync)

수동 동기화가 필요한 경우에만 사용:

{
  "type": "timer.sync",
  "payload": {
    "timer_id": "timer-uuid",  // 선택: 특정 타이머 조회
    "scope": "active"           // 선택: "active" (기본값) | "all"
  }
}
}
필드 타입 기본값 설명
timer_id UUID - 특정 타이머 ID (생략 시 목록 조회)
scope string active active: 활성 타이머만, all: 모든 타이머

응답:

  • 단건 조회 (timer_id 지정): timer.updated 메시지
  • 목록 조회 (timer_id 생략): timer.sync_result 메시지

💡 Tip: 연결 시 자동 동기화가 되므로, 수동 sync는 재연결 후 상태 확인이 필요한 경우에만 사용하세요.


서버 → 클라이언트 메시지

타이머 생성됨 (timer.created)

{
  "type": "timer.created",
  "payload": {
    "timer": { /* Timer 객체 */ },
    "action": "start"
  },
  "from_user": "user-uuid",
  "timestamp": "2026-01-28T10:00:00Z"
}

타이머 업데이트됨 (timer.updated)

{
  "type": "timer.updated",
  "payload": {
    "timer": { /* Timer 객체 */ },
    "action": "pause"  // "pause" | "resume" | "stop" | "sync"
  },
  "from_user": "user-uuid",
  "timestamp": "2026-01-28T10:30:00Z"
}

타이머 동기화 결과 (timer.sync_result)

{
  "type": "timer.sync_result",
  "payload": {
    "timers": [ /* Timer 객체 배열 */ ],
    "count": 2
  },
  "from_user": "user-uuid",
  "timestamp": "2026-01-28T10:30:00Z"
}

친구 활동 알림 (timer.friend_activity)

{
  "type": "timer.friend_activity",
  "payload": {
    "friend_id": "friend-user-uuid",
    "action": "start",
    "timer_id": "timer-uuid",
    "timer_title": "친구의 작업"
  },
  "from_user": "friend-user-uuid",
  "timestamp": "2026-01-28T10:00:00Z"
}

에러 (error)

{
  "type": "error",
  "payload": {
    "code": "PAUSE_FAILED",
    "message": "Cannot pause timer with status completed"
  },
  "timestamp": "2026-01-28T10:00:00Z"
}

REST API (조회/삭제만)

주의

타이머 생성, 일시정지, 재개, 종료는 WebSocket으로만 가능합니다.

Base URL

/v1

타이머 목록 조회

GET /v1/timers

Query Parameters:

파라미터 타입 기본값 설명
scope string mine 조회 범위: mine, shared, all
status string[] - 상태 필터 (RUNNING, PAUSED, COMPLETED, CANCELLED)
type string - 타입 필터: independent, schedule, todo
start_date datetime - 시작 날짜 필터
end_date datetime - 종료 날짜 필터
include_schedule boolean false Schedule 정보 포함
include_todo boolean false Todo 정보 포함
tag_include_mode string none 태그 포함 모드
timezone string UTC 타임존

현재 활성 타이머 조회

GET /v1/timers/active

활성 타이머가 없으면 404 Not Found 반환

타이머 상세 조회

GET /v1/timers/{timer_id}

타이머 메타데이터 업데이트

PATCH /v1/timers/{timer_id}
Content-Type: application/json

{
  "title": "업데이트된 제목",
  "description": "업데이트된 설명",
  "tag_ids": ["tag-uuid"]
}

타이머 삭제

DELETE /v1/timers/{timer_id}

TypeScript 타입 정의

// ============================================================
// Enums
// ============================================================

export type TimerStatus = "RUNNING" | "PAUSED" | "COMPLETED" | "CANCELLED";
export type TimerAction = "start" | "pause" | "resume" | "stop" | "cancel" | "sync";
export type WSMessageType = 
  | "timer.create" | "timer.pause" | "timer.resume" | "timer.stop" | "timer.sync"
  | "timer.created" | "timer.updated" | "timer.deleted" | "timer.sync_result" | "timer.friend_activity"
  | "connected" | "error";

// ============================================================
// Timer Types
// ============================================================

export interface PauseEvent {
  action: TimerAction;
  at: string;
  elapsed?: number;
}

export interface Timer {
  id: string;
  schedule_id?: string;
  todo_id?: string;
  title?: string;
  description?: string;
  allocated_duration: number;
  elapsed_time: number;
  status: TimerStatus;
  started_at?: string;
  paused_at?: string;
  ended_at?: string;
  pause_history: PauseEvent[];
  created_at: string;
  updated_at: string;
  schedule?: Schedule;
  todo?: Todo;
  tags: Tag[];
  owner_id?: string;
  is_shared: boolean;
}

export interface TimerCreate {
  schedule_id?: string;
  todo_id?: string;
  title?: string;
  description?: string;
  allocated_duration: number;
  tag_ids?: string[];
}

// ============================================================
// WebSocket Messages
// ============================================================

export interface WSClientMessage {
  type: WSMessageType;
  payload: Record<string, unknown>;
}

export interface WSServerMessage {
  type: WSMessageType;
  payload: Record<string, unknown>;
  from_user?: string;
  timestamp: string;
}

export interface TimerUpdatedPayload {
  timer: Timer;
  action: TimerAction;
}

export interface FriendActivityPayload {
  friend_id: string;
  action: TimerAction;
  timer_id: string;
  timer_title?: string;
}

export interface TimerSyncResultPayload {
  timers: Timer[];
  count: number;
}

export interface ErrorPayload {
  code: string;
  message: string;
}

사용 예시

WebSocket 연결 및 타이머 제어

class TimerWebSocket {
  private ws: WebSocket | null = null;
  private reconnectAttempts = 0;
  private maxReconnectAttempts = 5;

  constructor(
    private token: string,
    private onMessage: (msg: WSServerMessage) => void,
    private onError?: (error: Event) => void,
    private timezone?: string,  // 타임존 (예: "Asia/Seoul", "+09:00")
  ) {}

  connect(): void {
    // 환경에 따라 ws:// 또는 wss:// 사용
    const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
    const host = window.location.host; // 또는 명시적으로 API 서버 주소 지정

    // timezone만 쿼리 파라미터로 (토큰은 헤더로)
    const params = new URLSearchParams();
    if (this.timezone) {
      params.append('timezone', this.timezone);
    }
    const queryString = params.toString();
    const wsUrl = `${protocol}//${host}/v1/ws/timers${queryString ? '?' + queryString : ''}`;

    // 🔐 토큰은 반드시 Sec-WebSocket-Protocol 헤더로 전달 (보안)
    this.ws = new WebSocket(wsUrl, [
      `authorization.bearer.${this.token}`
    ]);

    this.ws.onopen = () => {
      console.log('Timer WebSocket connected');
      this.reconnectAttempts = 0;
    };

    this.ws.onmessage = (event) => {
      const message: WSServerMessage = JSON.parse(event.data);
      this.onMessage(message);
    };

    this.ws.onerror = (error) => {
      console.error('WebSocket error:', error);
      this.onError?.(error);
    };

    this.ws.onclose = () => {
      console.log('WebSocket closed');
      this.attemptReconnect();
    };
  }

  private attemptReconnect(): void {
    if (this.reconnectAttempts < this.maxReconnectAttempts) {
      this.reconnectAttempts++;
      const delay = Math.pow(2, this.reconnectAttempts) * 1000;
      setTimeout(() => this.connect(), delay);
    }
  }

  private send(message: WSClientMessage): void {
    if (this.ws?.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify(message));
    }
  }

  createTimer(data: TimerCreate): void {
    this.send({
      type: 'timer.create',
      payload: data,
    });
  }

  pauseTimer(timerId: string): void {
    this.send({
      type: 'timer.pause',
      payload: { timer_id: timerId },
    });
  }

  resumeTimer(timerId: string): void {
    this.send({
      type: 'timer.resume',
      payload: { timer_id: timerId },
    });
  }

  stopTimer(timerId: string): void {
    this.send({
      type: 'timer.stop',
      payload: { timer_id: timerId },
    });
  }

  syncTimer(timerId?: string): void {
    this.send({
      type: 'timer.sync',
      payload: timerId ? { timer_id: timerId } : {},
    });
  }

  disconnect(): void {
    this.ws?.close();
    this.ws = null;
  }
}

React Hook 예시

import { useState, useEffect, useCallback, useRef } from 'react';

function useTimerWebSocket(token: string, timezone?: string) {
  const [activeTimers, setActiveTimers] = useState<Timer[]>([]);
  const [friendActivity, setFriendActivity] = useState<FriendActivityPayload | null>(null);
  const [connected, setConnected] = useState(false);
  const [synced, setSynced] = useState(false);  // 초기 동기화 완료 여부
  const [error, setError] = useState<string | null>(null);
  const wsRef = useRef<TimerWebSocket | null>(null);

  useEffect(() => {
    const handleMessage = (msg: WSServerMessage) => {
      switch (msg.type) {
        case 'connected':
          setConnected(true);
          break;
        case 'timer.sync_result':
          // 자동 동기화 또는 수동 sync 응답
          const syncPayload = msg.payload as TimerSyncResultPayload;
          setActiveTimers(syncPayload.timers);
          setSynced(true);
          break;
        case 'timer.created':
        case 'timer.updated':
          const payload = msg.payload as TimerUpdatedPayload;
          // 활성 타이머 목록 업데이트
          setActiveTimers(prev => {
            const filtered = prev.filter(t => t.id !== payload.timer.id);
            if (payload.timer.status === 'RUNNING' || payload.timer.status === 'PAUSED') {
              return [...filtered, payload.timer];
            }
            return filtered;
          });
          break;
        case 'timer.friend_activity':
          setFriendActivity(msg.payload as FriendActivityPayload);
          setTimeout(() => setFriendActivity(null), 3000);
          break;
        case 'error':
          const errorPayload = msg.payload as ErrorPayload;
          setError(errorPayload.message);
          break;
      }
    };

    wsRef.current = new TimerWebSocket(token, handleMessage, undefined, timezone);
    wsRef.current.connect();

    return () => {
      wsRef.current?.disconnect();
    };
  }, [token, timezone]);

  const createTimer = useCallback((data: TimerCreate) => {
    wsRef.current?.createTimer(data);
  }, []);

  const pauseTimer = useCallback((timerId: string) => {
    wsRef.current?.pauseTimer(timerId);
  }, []);

  const resumeTimer = useCallback((timerId: string) => {
    wsRef.current?.resumeTimer(timerId);
  }, []);

  const stopTimer = useCallback((timerId: string) => {
    wsRef.current?.stopTimer(timerId);
  }, []);

  const syncTimer = useCallback((scope: 'active' | 'all' = 'active') => {
    wsRef.current?.send({
      type: 'timer.sync',
      payload: { scope },
    });
  }, []);

  return {
    activeTimers,  // 활성 타이머 목록
    friendActivity,
    connected,
    synced,  // 초기 동기화 완료 여부
    error,
    createTimer,
    pauseTimer,
    resumeTimer,
    stopTimer,
    syncTimer,
  };
}

// 사용 예시
function TimerComponent() {
  const {
    activeTimers,
    friendActivity,
    connected,
    synced,
    createTimer,
    pauseTimer,
    resumeTimer,
    stopTimer,
  } = useTimerWebSocket(authToken, 'Asia/Seoul');  // 타임존 지정

  if (!connected) return <div>연결 ...</div>;
  if (!synced) return <div>동기화 ...</div>;

  return (
    <div>
      {friendActivity && (
        <div className="notification">
          친구 {friendActivity.friend_id} 타이머를 {friendActivity.action}했습니다!
        </div>
      )}

      {activeTimers.length > 0 ? (
        <div>
          <h3>활성 타이머 ({activeTimers.length})</h3>
          {activeTimers.map(timer => (
            <div key={timer.id}>
              <h4>{timer.title || '타이머'}</h4>
              <p>상태: {timer.status}</p>
              <p>경과: {Math.floor(timer.elapsed_time / 60)}</p>
              {/* datetime 필드는 이미 Asia/Seoul 타임존으로 변환됨 */}
              <p>시작: {new Date(timer.started_at).toLocaleString('ko-KR')}</p>

              {timer.status === 'RUNNING' && (
                <button onClick={() => pauseTimer(timer.id)}>일시정지</button>
              )}
              {timer.status === 'PAUSED' && (
                <>
                  <button onClick={() => resumeTimer(timer.id)}>재개</button>
                  <button onClick={() => stopTimer(timer.id)}>종료</button>
                </>
              )}
            </div>
          ))}
        </div>
      ) : (
        <button onClick={() => createTimer({
          allocated_duration: 1800,
          title: '포모도로'
        })}>
          타이머 시작
        </button>
      )}
    </div>
  );
}

주의사항

1. WebSocket 연결 필수

타이머 제어 작업(생성, 일시정지, 재개, 종료)은 WebSocket 연결이 필수입니다. REST API로는 조회/삭제/메타데이터 업데이트만 가능합니다.

2. 🔐 보안: JWT 전달 방법

중요: WebSocket 연결 시 JWT는 반드시 Sec-WebSocket-Protocol 헤더로 전달해야 합니다.

// ✅ 올바른 방법: Sec-WebSocket-Protocol 헤더 사용
const ws = new WebSocket(
  'wss://api.example.com/v1/ws/timers?timezone=Asia/Seoul',
  ['authorization.bearer.' + jwtToken]
);

// ❌ 지원 안 함: 쿼리 파라미터로 토큰 전달 (보안 위험!)
// ws://host/v1/ws/timers?token=<JWT>

쿼리 파라미터로 토큰을 전달하면 안 되는 이유: - 서버 로그에 JWT 노출 - 브라우저 히스토리에 JWT 저장 - Referer 헤더를 통한 JWT 유출 가능 - 프록시/게이트웨이 로그에 기록됨

4. CORS 설정 필수

WebSocket 연결이 작동하지 않는다면 백엔드의 CORS 설정을 확인하세요:

문제 증상: - WebSocket 연결 시 에러 코드 1006 (비정상 종료) - 브라우저 콘솔에 CORS 에러 - 연결이 즉시 끊김

해결 방법:

백엔드 서버의 .env 파일 또는 환경변수에서 CORS_ALLOWED_ORIGINS를 설정하세요:

CORS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:8000,http://127.0.0.1:3000,http://127.0.0.1:8000,ws://localhost:8000,ws://127.0.0.1:8000
CORS_ALLOWED_ORIGINS=https://example.com,https://app.example.com,wss://api.example.com

WebSocket URL(ws:// 또는 wss://)도 반드시 포함해야 합니다!

5. 멀티 플랫폼 동기화

같은 사용자가 여러 기기에서 접속한 경우: - 한 기기에서 타이머를 일시정지하면 다른 기기에도 즉시 반영됩니다 - 새 기기 연결 시 자동으로 활성 타이머가 전송됩니다 (수동 sync 불필요) - WebSocket 연결이 끊어진 기기는 재연결 시 자동 동기화로 상태를 복구합니다

6. 친구 알림

  • 친구가 타이머를 시작/일시정지/재개/종료하면 timer.friend_activity 메시지를 받습니다
  • 알림은 WebSocket에 연결된 온라인 친구에게만 전송됩니다

7. pause_history 활용

// 총 작업 시간 계산
function getTotalWorkTime(history: PauseEvent[]): number {
  let totalWork = 0;
  let lastStart: Date | null = null;

  for (const event of history) {
    if (event.action === 'start' || event.action === 'resume') {
      lastStart = new Date(event.at);
    } else if ((event.action === 'pause' || event.action === 'stop') && lastStart) {
      const endTime = new Date(event.at);
      totalWork += (endTime.getTime() - lastStart.getTime()) / 1000;
      lastStart = null;
    }
  }

  return totalWork;
}

// 일시정지 횟수 계산
function getPauseCount(history: PauseEvent[]): number {
  return history.filter(e => e.action === 'pause').length;
}

8. 연결 재시도

WebSocket 연결이 끊어진 경우 지수 백오프로 재연결을 시도하세요:

const delay = Math.pow(2, attempt) * 1000;  // 2초, 4초, 8초, 16초...

9. 타이머 상태 전이

stateDiagram-v2
    [*] --> RUNNING
    RUNNING --> PAUSED: pause
    PAUSED --> RUNNING: resume
    PAUSED --> COMPLETED: stop
    RUNNING --> CANCELLED: cancel

10. 접근권한(Visibility) 설정

타이머의 접근권한은 WebSocket이 아닌 별도의 REST API로 설정합니다. timer.create 페이로드에는 visibility 필드가 없으므로, 생성 후 필요한 경우에만 호출합니다.

PUT /v1/visibility/timer/{timer_id}
{
  "level": "friends"
}

권장: Lazy Visibility

타이머는 개인 집중 도구이므로 기본값 PRIVATE이 자연스럽습니다. 생성 시점에 즉시 설정하기보다, 사용자가 "공유" 버튼을 누를 때만 호출하세요.

자세한 내용은 Visibility API 가이드를 참고하세요.


API 요약

WebSocket API

방향 메시지 타입 설명
timer.create 타이머 생성
timer.pause 타이머 일시정지
timer.resume 타이머 재개
timer.stop 타이머 종료
timer.sync 타이머 동기화 요청
connected 연결 성공
timer.sync_result 타이머 목록 (자동/수동)
timer.created 타이머 생성됨
timer.updated 타이머 업데이트됨
timer.friend_activity 친구 활동 알림
error 에러

REST API

Method Endpoint 설명
GET /v1/timers 타이머 목록 조회
GET /v1/timers/active 현재 활성 타이머 조회
GET /v1/timers/{id} 타이머 상세 조회
PATCH /v1/timers/{id} 타이머 메타데이터 업데이트
DELETE /v1/timers/{id} 타이머 삭제
GET /v1/schedules/{id}/timers Schedule의 타이머 조회
GET /v1/todos/{id}/timers Todo의 타이머 조회
PUT /v1/visibility/timer/{id} 타이머 접근권한 설정
GET /v1/visibility/timer/{id} 타이머 접근권한 조회
DELETE /v1/visibility/timer/{id} 타이머 접근권한 삭제 (PRIVATE 복귀)

관련 문서