チュートリアル

omnilude-tools の実装に使われたPRD文書の一部を紹介します

AAnonymous
10分で読めます

はじめに

前回紹介した omnilude-tools には、実装に直接つながったPRD文書がいくつもあります。今回公開する文書は、そのうちのひとつである Unix Timestamp Converter PRDです。

この文書は、実際の timestamp-converter ツールを作るために韓国語で先に書かれたもので、現在の実装の痕跡も src/app/[locale]/(tools)/developer/timestamp-converter/ に残っています。つまり、単なるアイデアメモではなく、画面構成、状態管理、SEO、テスト設計までを一度に束ねた作業文書でした。

この投稿で特に明確に残しておきたい点は3つあります。

  • この文書は、前回紹介した omnilude-tools の中で実際のツールを作るときに使われたPRD文書のひとつです。
  • この文書は、prd-generator スキルを実戦で活用した良い事例でもあります。
  • このPRDの原文は韓国語で書かれており、この投稿では各言語の読者が文書全体を読めるよう、翻訳も一緒に整理しています。

現在このツールは tools.omnilude.com/developer/timestamp-converter でも直接確認できます。

できればこのPRDを生成したときに使ったプロンプトもあわせて公開したかったのですが、ディスク容量の問題でセッションデータを削除してしまったため、ここで提供できない点はご了承ください。

本論

短く言えば、この文書は実装者がすぐに動けるレベルまで範囲を絞ってくれました。

  • 何を作るか: Unixタイムスタンプと人が読める日付を双方向で変換するツール
  • どこまで作るか: MVP、高度機能、タイムゾーン、現在時刻のリアルタイム表示、コードスニペット、計算機
  • どう作るか: ファイル構成、Zustand状態管理、変換ユーティリティ、SEO、テスト、アクセシビリティ、性能要件
  • どう検証するか: 単体テスト、E2Eテスト、実装チェックリスト

こうした文書があると、実装はかなり単純になります。実際に timestamp-converter ページはこのような構造的ガイドを土台に実装され、今も omnilude-tools の developer グループのツールとして残っています。

この文書は韓国語から翻訳されました

この投稿の基準原文は韓国語のPRDです。そのため各言語の翻訳版でも、要約ではなく下の文書全体を読めるように移すことを重視しました。PRD文書だからといって大まかに縮めるのではなく、実際の実装背景と技術的文脈が保たれることを優先しました。

PRD原文

以下は公開用に整理した Unix Timestamp Converter PRDの全文です。

PRD: Unix Timestamp Converter

基本情報

項目
ツール IDtimestamp-converter
グループdeveloper
パス/developer/timestamp-converter
優先度P0 (必須)
想定工数1-2日
状態Draft

1. 概要

1.1 目的

Unixタイムスタンプと人が読める日付形式の間で双方向の変換を提供するツール。

1.2 対象ユーザー

  • バックエンド/フロントエンド開発者
  • データアナリスト
  • システム管理者

1.3 競合分析

サイト長所短所
epochconverter.com多様な形式、コードスニペットUIが古い
unixtimestamp.comシンプルなUI機能が限定的
timestamp-converter.comすっきりしたデザインタイムゾーンが限定的

2. 機能要件

2.1 コア機能 (MVP)

F1: タイムスタンプ → 日付変換

Typescript
入力: Unixタイムスタンプ (秒/ミリ秒/マイクロ秒を自動判定)
出力: 複数の日付形式

自動判定ロジック:

Typescript
function detectTimestampUnit(value: number): 'seconds' | 'milliseconds' | 'microseconds' {
  if (value < 1e12) return 'seconds';       // 10桁未満
  if (value < 1e15) return 'milliseconds';  // 13桁
  return 'microseconds';                    // 16桁
}

出力形式:

  • ISO 8601: 2026-01-29T13:45:30.000Z
  • RFC 2822: Wed, 29 Jan 2026 13:45:30 +0000
  • ローカル形式: 2026年1月29日 22:45:30
  • 相対時間: 5分前, 3日後

F2: 日付 → タイムスタンプ変換

Typescript
入力: 日付文字列または日時選択
出力: Unixタイムスタンプ (秒、ミリ秒)

対応入力形式:

  • ISO 8601 文字列
  • 日付/時刻ピッカー (DatePicker)
  • 自然言語 (任意): now, yesterday, next week

F3: タイムゾーン対応

Typescript
デフォルト: ブラウザのローカルタイムゾーン
オプション: UTC、主要都市のタイムゾーン選択

主要タイムゾーン一覧:

Typescript
const TIMEZONES = [
  { value: 'UTC', label: 'UTC' },
  { value: 'Asia/Seoul', label: 'ソウル (KST, UTC+9)' },
  { value: 'Asia/Tokyo', label: '東京 (JST, UTC+9)' },
  { value: 'America/New_York', label: 'ニューヨーク (EST/EDT, UTC-5/-4)' },
  { value: 'America/Los_Angeles', label: 'ロサンゼルス (PST/PDT, UTC-8/-7)' },
  { value: 'Europe/London', label: 'ロンドン (GMT/BST, UTC+0/+1)' },
  // ... さらに追加
];

F4: 現在時刻のリアルタイム表示

Typescript
画面上部に現在の Unix タイムスタンプを 1 秒間隔で更新して表示

2.2 高度機能

F5: コードスニペット生成

選択したタイムスタンプを複数のプログラミング言語コードで提供する。

Typescript
const CODE_SNIPPETS = {
  javascript: (ts: number) => `new Date(${ts * 1000})`,
  python: (ts: number) => `datetime.fromtimestamp(${ts})`,
  java: (ts: number) => `Instant.ofEpochSecond(${ts}L)`,
  go: (ts: number) => `time.Unix(${ts}, 0)`,
  php: (ts: number) => `date('Y-m-d H:i:s', ${ts})`,
  ruby: (ts: number) => `Time.at(${ts})`,
  csharp: (ts: number) => `DateTimeOffset.FromUnixTimeSeconds(${ts})`,
};

F6: タイムスタンプ計算機

Typescript
基準時刻 + 日/時/分/秒 = 結果タイムスタンプ
例: 現在 + 7日 = 来週のタイムスタンプ

3. UI/UX 設計

3.1 レイアウト

┌─────────────────────────────────────────────────────────────┐
│  現在の Unix タイムスタンプ: 1738150800 (リアルタイム更新) [コピー] │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────────────┐  ┌─────────────────────────────┐  │
│  │  入力               │  │  結果                        │  │
│  │                     │  │                              │  │
│  │  [タイムスタンプ]   │  │  ISO 8601: ...      [コピー]  │  │
│  │  または             │  │  RFC 2822: ...      [コピー]  │  │
│  │  [日時選択]         │  │  ローカル: ...      [コピー]  │  │
│  │                     │  │  相対: ...          [コピー]  │  │
│  │  タイムゾーン: [▼]  │  │                              │  │
│  │                     │  │  タイムスタンプ (秒): ...     │  │
│  │  [変換] [クリア]    │  │  タイムスタンプ (ms): ...     │  │
│  └─────────────────────┘  └─────────────────────────────┘  │
│                                                             │
├─────────────────────────────────────────────────────────────┤
│  コードスニペット                                           │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  [JS] [Python] [Java] [Go] [PHP] [Ruby] [C#]        │   │
│  │  ─────────────────────────────────────────────────  │   │
│  │  new Date(1738150800000)                   [コピー] │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

3.2 レスポンシブデザイン

Desktop (lg+):

  • 2カラムレイアウト (入力 | 結果)

Tablet/Mobile (< lg):

  • 1カラムレイアウト (上に入力、下に結果)

3.3 インタラクション

  1. リアルタイム変換: 入力時に自動変換 (デバウンス 300ms)
  2. コピー時のフィードバック: コピーボタンを押すと toast 通知
  3. キーボードショートカット: Enter で変換、Ctrl+C で結果コピー

4. 技術仕様

4.1 ファイル構造

src/app/[locale]/(tools)/developer/timestamp-converter/
├── page.tsx                              # ページエントリ + SEO
├── _store/
│   └── timestamp-store.ts                # Zustand 状態管理
└── _components/
    ├── timestamp-page.tsx                # メインクライアントコンポーネント
    ├── current-time-display.tsx          # 現在時刻表示
    ├── timestamp-input.tsx               # 入力セクション
    ├── conversion-result.tsx             # 結果セクション
    └── code-snippets.tsx                 # コードスニペットセクション

4.2 状態管理 (Zustand)

Typescript
// _store/timestamp-store.ts
import { create } from 'zustand';

type InputMode = 'timestamp' | 'datetime';

interface TimestampState {
  // 入力
  inputMode: InputMode;
  timestampInput: string;
  dateInput: Date | null;
  timezone: string;

  // 結果
  result: ConversionResult | null;

  // コードスニペット
  selectedLanguage: string;

  // アクション
  setInputMode: (mode: InputMode) => void;
  setTimestampInput: (value: string) => void;
  setDateInput: (date: Date | null) => void;
  setTimezone: (tz: string) => void;
  setSelectedLanguage: (lang: string) => void;
  convert: () => void;
  clear: () => void;
}

interface ConversionResult {
  timestampSeconds: number;
  timestampMillis: number;
  iso8601: string;
  rfc2822: string;
  localFormat: string;
  relative: string;
}

export const useTimestampStore = create<TimestampState>((set, get) => ({
  inputMode: 'timestamp',
  timestampInput: '',
  dateInput: null,
  timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
  result: null,
  selectedLanguage: 'javascript',

  setInputMode: (mode) => set({ inputMode: mode }),
  setTimestampInput: (value) => set({ timestampInput: value }),
  setDateInput: (date) => set({ dateInput: date }),
  setTimezone: (tz) => set({ timezone: tz }),
  setSelectedLanguage: (lang) => set({ selectedLanguage: lang }),

  convert: () => {
    const { inputMode, timestampInput, dateInput, timezone } = get();
    // 変換ロジック
  },

  clear: () => set({
    timestampInput: '',
    dateInput: null,
    result: null,
  }),
}));

4.3 コア変換ロジック

Typescript
// lib/timestamp-utils.ts
import { format, formatDistanceToNow } from 'date-fns';
import { formatInTimeZone } from 'date-fns-tz';
import { ko, en, ja } from 'date-fns/locale';

export function convertTimestamp(
  timestamp: number,
  timezone: string,
  locale: string
): ConversionResult {
  // 単位を自動判定
  const unit = detectTimestampUnit(timestamp);
  const timestampMs = unit === 'seconds'
    ? timestamp * 1000
    : unit === 'microseconds'
      ? Math.floor(timestamp / 1000)
      : timestamp;

  const date = new Date(timestampMs);
  const localeObj = { ko, en, ja }[locale] || ko;

  return {
    timestampSeconds: Math.floor(timestampMs / 1000),
    timestampMillis: timestampMs,
    iso8601: date.toISOString(),
    rfc2822: format(date, 'EEE, dd MMM yyyy HH:mm:ss xx', { locale: localeObj }),
    localFormat: formatInTimeZone(date, timezone, 'PPpp', { locale: localeObj }),
    relative: formatDistanceToNow(date, { addSuffix: true, locale: localeObj }),
  };
}

export function dateToTimestamp(date: Date): { seconds: number; millis: number } {
  const millis = date.getTime();
  return {
    seconds: Math.floor(millis / 1000),
    millis,
  };
}

4.4 依存関係

JSON
{
  "dependencies": {
    "date-fns": "^3.x",
    "date-fns-tz": "^3.x"
  }
}

プロジェクトにはすでに導入済みです。

5. 多言語対応

5.1 翻訳キー

JSON
// messages/ja/tools/developer/timestamp-converter.json
{
  "tools": {
    "developer": {
      "timestampConverter": "タイムスタンプ変換器",
      "timestampConverterDesc": "Unix タイムスタンプと日付を相互変換します",
      "timestamp": {
        "currentTime": "現在の Unix タイムスタンプ",
        "inputTimestamp": "タイムスタンプ入力",
        "inputDatetime": "日付/時刻入力",
        "timezone": "タイムゾーン",
        "convert": "変換",
        "clear": "クリア",
        "copy": "コピー",
        "copied": "コピーしました",
        "results": "変換結果",
        "iso8601": "ISO 8601",
        "rfc2822": "RFC 2822",
        "localFormat": "ローカル形式",
        "relative": "相対時間",
        "timestampSeconds": "タイムスタンプ (秒)",
        "timestampMillis": "タイムスタンプ (ミリ秒)",
        "codeSnippets": "コードスニペット",
        "invalidTimestamp": "有効ではないタイムスタンプです",
        "autoDetected": "自動検出: {unit}"
      }
    }
  }
}

6. SEO メタデータ

Typescript
// page.tsx
export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { locale } = await params;
  const t = await getTranslations({ locale });

  return generateSeoMetadata({
    locale,
    title: t('tools.developer.timestampConverter'),
    description: t('tools.developer.timestampConverterDesc'),
    path: '/developer/timestamp-converter',
    keywords: [
      'unix timestamp',
      'epoch converter',
      'タイムスタンプ変換',
      '日付変換',
      'timestamp to date',
      'date to timestamp',
    ],
  });
}

7. テストケース

7.1 単体テスト

Typescript
describe('detectTimestampUnit', () => {
  it('should detect seconds', () => {
    expect(detectTimestampUnit(1738150800)).toBe('seconds');
  });

  it('should detect milliseconds', () => {
    expect(detectTimestampUnit(1738150800000)).toBe('milliseconds');
  });

  it('should detect microseconds', () => {
    expect(detectTimestampUnit(1738150800000000)).toBe('microseconds');
  });
});

describe('convertTimestamp', () => {
  it('should convert timestamp to ISO 8601', () => {
    const result = convertTimestamp(1738150800, 'UTC', 'en');
    expect(result.iso8601).toBe('2025-01-29T09:00:00.000Z');
  });
});

7.2 E2E テスト

Typescript
test('タイムスタンプ変換フロー', async ({ page }) => {
  await page.goto('/ja/developer/timestamp-converter');

  // タイムスタンプ入力
  await page.fill('[data-testid="timestamp-input"]', '1738150800');

  // 結果確認
  await expect(page.locator('[data-testid="iso8601-result"]'))
    .toContainText('2025-01-29');

  // コピーボタンをクリック
  await page.click('[data-testid="copy-iso8601"]');
  await expect(page.locator('.toast')).toContainText('コピーしました');
});

8. アクセシビリティ要件

  • すべての入力フィールドに適切な label
  • コピーボタンに aria-label
  • キーボードナビゲーション対応
  • スクリーンリーダー互換性

9. 性能要件

  • 入力後の変換結果表示: < 100ms
  • 現在時刻の更新: 毎秒正確に
  • バンドルサイズ増加: < 5KB (gzip)

10. 実装チェックリスト

  • ファイル構造作成
  • Zustand ストア実装
  • 変換ユーティリティ関数実装
  • メインページコンポーネント実装
  • 現在時刻表示コンポーネント
  • 入力セクションコンポーネント
  • 結果セクションコンポーネント
  • コードスニペットセクションコンポーネント
  • 多言語翻訳キー追加 (7言語)
  • tools.ts にツール登録
  • レスポンシブスタイリング
  • アクセシビリティ確認
  • テスト作成