マイページAPI設計:購入履歴・写真閲覧システム
🎯 マイページ要件
ユーザー体験設計
const mypageUX = {
// 🖼️ 核心機能:購入した写真の完全表示
photoDisplay: {
requirement: "購入したすべての写真を美しく表示",
features: [
"高画質プレビュー(写真クリックで拡大)",
"サムネイルグリッド表示",
"ブランド別フィルタリング",
"日付・価格でのソート",
"写真のダウンロード機能"
]
},
// 📋 購入履歴管理
orderHistory: {
requirement: "包括的な注文履歴管理",
features: [
"全ブランド統合ビュー + ブランド別ビュー",
"注文ステータスリアルタイム表示",
"配送追跡情報",
"再注文ワンクリック機能",
"領収書・請求書ダウンロード"
]
},
// ⚡ パフォーマンス要件
performance: {
initialLoad: "3秒以内",
photoThumbnails: "1秒以内",
highResPreview: "2秒以内",
infiniteScroll: "スムーズな継続読み込み"
}
}
🛠️ API エンドポイント設計
1. 認証・ユーザー情報 API
// 🔐 ユーザー認証・基本情報
interface AuthAPI {
// ユーザー基本情報取得
"GET /api/mypage/profile": {
headers: {
"Authorization": "Bearer <firebase_token>"
},
response: {
user: {
firebase_uid: string,
shopify_customer_id: string,
email: string,
display_name: string,
// マルチブランド利用状況
brand_activity: {
neko: {
member_since: string,
total_orders: number,
total_spent: number,
favorite_finish: "matte" | "glossy" | "premium"
},
tokinoe: BrandActivity,
dog: BrandActivity
},
// マイページ設定
preferences: {
preferred_brand: Brand,
photo_grid_size: "small" | "medium" | "large",
notifications: {
order_updates: boolean,
promotions: boolean
}
}
}
}
},
// プロフィール更新
"PUT /api/mypage/profile": {
headers: {
"Authorization": "Bearer <firebase_token>"
},
body: {
display_name?: string,
preferences?: UserPreferences
},
response: {
success: boolean,
updated_fields: string[]
}
}
}
2. 注文履歴 API
// 📋 注文履歴管理API
interface OrderHistoryAPI {
// 注文履歴取得(ページネーション対応)
"GET /api/mypage/orders": {
headers: {
"Authorization": "Bearer <firebase_token>"
},
query: {
brand?: "all" | "neko" | "tokinoe" | "dog",
status?: "all" | "processing" | "shipped" | "delivered",
limit?: number, // デフォルト20
cursor?: string, // ページネーション用
sort?: "newest" | "oldest" | "amount_high" | "amount_low"
},
response: {
orders: EnhancedOrder[],
pagination: {
has_more: boolean,
next_cursor: string | null,
total_count: number
},
summary: {
total_orders: number,
total_spent: number,
favorite_items: string[]
}
}
},
// 注文詳細取得
"GET /api/mypage/orders/{order_id}": {
headers: {
"Authorization": "Bearer <firebase_token>"
},
response: {
order: {
// Shopify基本情報
id: string,
name: string, // "#1001"
created_at: string,
updated_at: string,
total_price: number,
currency: "JPY",
// ブランド情報(メタフィールドから)
brand: {
id: "neko" | "tokinoe" | "dog",
display_name: string,
theme_color: string
},
// ステータス情報
status: {
financial: "pending" | "paid" | "refunded",
fulfillment: "unfulfilled" | "fulfilled" | "shipped",
processing: "uploaded" | "processing" | "printed" | "ready",
// 詳細ステータス(メタフィールドから)
details: {
estimated_completion: string,
quality_check_status: "pending" | "passed" | "review_required",
print_queue_position: number | null
}
},
// 配送情報
shipping: {
address: ShippingAddress,
tracking?: {
number: string,
carrier: string,
url: string,
status: "in_transit" | "delivered" | "exception"
}
},
// 注文商品(写真情報付き)
line_items: LineItemWithPhoto[]
}
}
}
}
// 🖼️ 写真情報付きラインアイテム
interface LineItemWithPhoto {
id: string,
quantity: number,
price: number,
// 商品基本情報
product: {
title: string,
variant_title: string
},
// 📸 写真情報(メタフィールドから)
photo: {
// R2ストレージ情報
original: {
url: string, // 署名付きURL(24時間有効)
filename: string,
file_size: number,
dimensions: { width: number, height: number }
},
// プレビュー・サムネイル
thumbnails: {
small: string, // 200x200
medium: string, // 400x400
large: string // 800x800
},
// 印刷用画像
print_ready: {
url: string,
processing_applied: string[], // ["color_correction", "noise_reduction"]
print_specs: PrintSpecs
}
},
// 🎨 プリント仕様
print_specs: {
size: "89x89mm",
finish: "matte" | "glossy" | "premium",
quantity: number,
border: boolean,
color_correction: "auto" | "manual" | "none"
}
}
3. 写真管理 API
// 🖼️ 写真管理・表示API
interface PhotoAPI {
// 写真ギャラリー取得
"GET /api/mypage/photos": {
headers: {
"Authorization": "Bearer <firebase_token>"
},
query: {
brand?: "all" | "neko" | "tokinoe" | "dog",
date_range?: {
start: string, // ISO date
end: string
},
sort?: "newest" | "oldest" | "most_ordered",
limit?: number,
cursor?: string
},
response: {
photos: PhotoSummary[],
pagination: PaginationInfo,
stats: {
total_photos: number,
total_prints: number,
favorite_finish: string
}
}
},
// 写真詳細・アクション
"GET /api/mypage/photos/{order_id}/{line_item_id}": {
headers: {
"Authorization": "Bearer <firebase_token>"
},
response: {
photo: {
// 基本情報
id: string,
original_filename: string,
upload_date: string,
// 画像URLs(署名付き)
urls: {
original: string, // オリジナル画像
display: string, // 表示用高画質
thumbnail: string, // サムネイル
print_ready: string // 印刷用最終版
},
// メタデータ
metadata: {
dimensions: { width: number, height: number },
file_size: number,
color_space: string,
processing_applied: string[]
},
// 注文履歴
order_history: {
order_id: string,
order_name: string, // "#1001"
ordered_date: string,
quantity: number,
finish: string,
total_paid: number,
status: OrderStatus
}[],
// アクション可能性
actions: {
can_reorder: boolean,
can_download_original: boolean,
can_share: boolean,
download_limits?: {
remaining: number,
reset_date: string
}
}
}
}
},
// 📥 オリジナル画像ダウンロード
"POST /api/mypage/photos/{order_id}/{line_item_id}/download": {
headers: {
"Authorization": "Bearer <firebase_token>"
},
response: {
download_url: string, // 署名付き一時URL(1時間有効)
expires_at: string,
file_size: number,
filename: string
}
}
}
4. 再注文・アクション API
// 🔄 再注文・アクションAPI
interface ActionAPI {
// 再注文(同じ仕様)
"POST /api/mypage/reorder": {
headers: {
"Authorization": "Bearer <firebase_token>"
},
body: {
source_order_id: string,
line_item_ids: string[], // 再注文する商品
// オプション:仕様変更
modifications?: {
quantity?: number,
finish?: "matte" | "glossy" | "premium",
rush_order?: boolean
}
},
response: {
// Shopifyドラフト注文作成
draft_order: {
id: string,
checkout_url: string, // 決済ページURL
total_price: number,
line_items: ReorderLineItem[]
},
// 処理情報
processing_info: {
estimated_completion: string,
rush_available: boolean,
rush_fee?: number
}
}
},
// お気に入り管理
"POST /api/mypage/favorites": {
headers: {
"Authorization": "Bearer <firebase_token>"
},
body: {
action: "add" | "remove",
order_id: string,
line_item_id: string
},
response: {
success: boolean,
favorites_count: number
}
},
// 写真シェア(家族・友人と共有)
"POST /api/mypage/photos/{order_id}/{line_item_id}/share": {
headers: {
"Authorization": "Bearer <firebase_token>"
},
body: {
share_type: "public_link" | "email",
recipients?: string[], // メール共有時
expiry_hours?: number, // デフォルト48時間
allow_download?: boolean // ダウンロード許可
},
response: {
share_url: string,
expires_at: string,
share_id: string
}
}
}
🏗️ データ取得アーキテクチャ
ハイブリッド取得戦略
// ⚡ 高速データ取得システム
interface DataRetrievalStrategy {
// L1: D1キャッシュ(高速)
cacheFirst: {
data: [
"基本注文情報",
"写真メタデータ",
"処理状態",
"サムネイルURL"
],
ttl: "セッション中有効",
fallback: "Shopify API"
},
// L2: Shopify API(正確性)
authoritative: {
data: [
"決済状態",
"配送情報",
"顧客情報",
"メタフィールド最新版"
],
cache_policy: "5分間キャッシュ",
rate_limit: "40req/min遵守"
},
// L3: R2署名付きURL(リアルタイム)
dynamicGeneration: {
data: [
"画像表示URL",
"ダウンロードURL"
],
validity: "1-24時間",
generation: "リクエスト時"
}
}
// 📊 最適化されたデータフロー
async function getMypageData(firebaseUID: string, filters: MypageFilters): Promise<MypageData> {
// 1. 顧客特定(Firebase UID → Shopify Customer ID)
const customerMapping = await getCustomerMappingFromD1(firebaseUID)
if (!customerMapping) {
throw new Error('Customer mapping not found')
}
// 2. 注文一覧取得(D1キャッシュ優先)
let orders = await getCachedOrdersFromD1(customerMapping.shopify_customer_id, filters)
if (!orders || orders.length === 0) {
// フォールバック:Shopify APIから取得
orders = await fetchOrdersFromShopify(customerMapping.shopify_customer_id, filters)
// D1キャッシュ更新
await updateOrderCacheInD1(orders)
}
// 3. 写真情報エンリッチ
const enrichedOrders = await Promise.all(
orders.map(async (order) => ({
...order,
line_items: await Promise.all(
order.line_items.map(async (item) => {
// R2署名付きURL生成
const photoURLs = await generatePhotoURLs(item.photo_file?.r2_key)
return {
...item,
photo: {
...item.photo_file,
urls: photoURLs
}
}
})
)
}))
)
return {
orders: enrichedOrders,
pagination: calculatePagination(orders, filters),
summary: calculateOrderSummary(orders)
}
}
R2署名付きURL管理
// 🔐 セキュアな画像アクセス管理
interface SecureImageAccess {
// 署名付きURL生成
generateSignedURLs: async (r2Key: string, accessLevel: AccessLevel) => {
const baseConfig = {
bucket: 'nekomata-photos',
region: 'auto',
credentials: R2_CREDENTIALS
}
switch (accessLevel) {
case 'thumbnail':
return await generateSignedURL(r2Key, {
expires: 3600, // 1時間
transform: 'w=400,h=400,f=webp'
})
case 'display':
return await generateSignedURL(r2Key, {
expires: 7200, // 2時間
transform: 'w=1200,h=1200,f=webp,q=90'
})
case 'download':
return await generateSignedURL(r2Key, {
expires: 1800, // 30分(ダウンロード用)
disposition: 'attachment'
})
case 'original':
return await generateSignedURL(r2Key, {
expires: 86400, // 24時間(プレミアム機能)
access_control: 'strict'
})
}
},
// アクセス権限チェック
validateImageAccess: async (firebaseUID: string, r2Key: string): Promise<boolean> => {
// 1. 画像の所有者確認
const photoOwnership = await checkPhotoOwnership(firebaseUID, r2Key)
if (!photoOwnership.isOwner) {
return false
}
// 2. アクセス制限チェック(ダウンロード回数など)
const accessLimits = await checkAccessLimits(firebaseUID, r2Key)
if (accessLimits.exceeded) {
return false
}
return true
}
}
📱 フロントエンド実装ガイド
React コンポーネント設計
// 🖼️ 写真ギャラリーコンポーネント
const PhotoGallery: React.FC<PhotoGalleryProps> = ({ brand, user }) => {
const [photos, setPhotos] = useState<PhotoSummary[]>([])
const [loading, setLoading] = useState(true)
const [selectedPhoto, setSelectedPhoto] = useState<string | null>(null)
// 無限スクロール対応
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery(
['mypage-photos', brand],
({ pageParam }) => fetchPhotos({ brand, cursor: pageParam }),
{
getNextPageParam: (lastPage) => lastPage.pagination.next_cursor
}
)
return (
<div className="photo-gallery">
{/* フィルター・ソート */}
<PhotoFilters
selectedBrand={brand}
onBrandChange={setBrand}
sortBy={sortBy}
onSortChange={setSortBy}
/>
{/* 写真グリッド */}
<div className="photo-grid">
{data?.pages.flatMap(page => page.photos).map(photo => (
<PhotoThumbnail
key={`${photo.order_id}-${photo.line_item_id}`}
photo={photo}
onClick={() => setSelectedPhoto(photo.id)}
onReorder={() => handleReorder(photo)}
/>
))}
</div>
{/* 詳細モーダル */}
{selectedPhoto && (
<PhotoDetailModal
photoId={selectedPhoto}
onClose={() => setSelectedPhoto(null)}
onDownload={handleDownload}
onShare={handleShare}
/>
)}
{/* 無限スクロール トリガー */}
{hasNextPage && (
<div ref={loadMoreRef}>
{isFetchingNextPage && <LoadingSpinner />}
</div>
)}
</div>
)
}
// 📋 注文履歴コンポーネント
const OrderHistory: React.FC<OrderHistoryProps> = ({ user }) => {
const [filter, setFilter] = useState<OrderFilter>({ brand: 'all', status: 'all' })
const { data: orders, isLoading } = useQuery(
['order-history', filter],
() => fetchOrderHistory(filter),
{ staleTime: 5 * 60 * 1000 } // 5分間キャッシュ
)
return (
<div className="order-history">
{/* サマリー統計 */}
<OrderSummaryCards orders={orders?.orders} />
{/* フィルター */}
<OrderFilters filter={filter} onChange={setFilter} />
{/* 注文一覧 */}
<div className="order-list">
{orders?.orders.map(order => (
<OrderCard
key={order.id}
order={order}
onViewPhotos={() => viewOrderPhotos(order.id)}
onReorder={() => handleReorder(order)}
/>
))}
</div>
</div>
)
}
🎯 パフォーマンス最適化
レスポンス時間目標
const performanceTargets = {
// 初期表示
initialPageLoad: "3秒以内",
photoThumbnailsLoad: "1秒以内",
orderHistoryLoad: "2秒以内",
// インタラクション
photoDetailOpen: "500ms以内",
filterChange: "300ms以内",
infiniteScrollNext: "800ms以内",
// 画像表示
thumbnailDisplay: "200ms以内",
highResPreview: "2秒以内",
originalDownload: "開始まで500ms以内"
}
キャッシュ戦略
const cachingStrategy = {
// ブラウザキャッシュ
browser: {
thumbnails: "24時間",
orderSummary: "5分",
userProfile: "セッション中"
},
// CDNキャッシュ(Cloudflare)
cdn: {
api_responses: "30秒",
thumbnails: "1時間",
static_assets: "24時間"
},
// アプリケーションキャッシュ
app: {
react_query: "5分",
photo_metadata: "10分",
user_preferences: "セッション中"
}
}
最終更新: 2025-08-23 18:14:53 JST
設計ステータス: マイページAPI完全設計完了
実装準備: フロントエンド・バックエンド同時実装可能