Skip to content

Conversation

@ikea-6330
Copy link
Contributor

No description provided.

@yas-ako yas-ako linked an issue Sep 4, 2025 that may be closed by this pull request
@yas-ako yas-ako requested a review from Copilot November 12, 2025 19:50
Copilot finished reviewing on behalf of yas-ako November 12, 2025 19:52
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds a ranking feature that displays stamp usage statistics in both total and monthly views. The implementation includes a new page component with loading states, error handling, and tabbed ranking tables, along with a dedicated composable for managing the ranking data fetching logic.

Key Changes:

  • New ranking page with lazy-loading approach (user clicks button to load data)
  • Composable for fetching and processing stamp ranking data from multiple API endpoints
  • Tabbed interface showing total and monthly usage rankings with pagination

Reviewed Changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 9 comments.

File Description
client/app/pages/ranking.vue New page component that displays stamp rankings with multiple UI states (initial, loading, error, data display) and tabbed views for total and monthly rankings
client/app/composables/useRanking.ts New composable that handles fetching ranking and stamp data, merges them, and provides reactive state management with error handling

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +142 to +143
:items-per-page="tableTotal?.tableApi?.getState().pagination.pageSize"
:total="tableTotal?.tableApi?.getFilteredRowModel().rows.length"
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accessing nested optional properties with optional chaining (tableTotal.value?.tableApi?.getState()...) could fail if tableApi is not yet initialized during the initial render. Consider adding a fallback value or wrapping this in a computed property that checks for existence before accessing these properties to prevent potential runtime errors.

Copilot uses AI. Check for mistakes.
<UPagination
v-model:page="pageMonthly"
:items-per-page="tableMonthly?.tableApi?.getState().pagination.pageSize"
:total="tableMonthly?.tableApi?.getFilteredRowModel().rows.length"
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the total ranking pagination, accessing nested optional properties with optional chaining could fail if tableApi is not yet initialized. Consider adding appropriate fallback values or null checks to prevent potential runtime errors.

Suggested change
:total="tableMonthly?.tableApi?.getFilteredRowModel().rows.length"
:total="tableMonthly?.tableApi?.getFilteredRowModel().rows.length ?? 0"

Copilot uses AI. Check for mistakes.
Comment on lines +222 to +228
const { ranking, loading, error, initialized, loadRanking, refresh } = useRanking();

// 初期状態にリセットする関数
const resetToInitial = () => {
// composable内部の状態をリセット(実装上は新しいインスタンスの作成が必要)
// ここでは簡易的にページリロード
window.location.reload();
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using window.location.reload() forces a full page reload, which is not ideal for a Vue/Nuxt SPA. Consider implementing a proper reset mechanism in the useRanking composable by adding a reset() method that clears the internal state, rather than forcing a page reload. This would provide a better user experience and follow Vue best practices.

Suggested change
const { ranking, loading, error, initialized, loadRanking, refresh } = useRanking();
// 初期状態にリセットする関数
const resetToInitial = () => {
// composable内部の状態をリセット(実装上は新しいインスタンスの作成が必要)
// ここでは簡易的にページリロード
window.location.reload();
const { ranking, loading, error, initialized, loadRanking, refresh, reset } = useRanking();
// 初期状態にリセットする関数
const resetToInitial = () => {
// composable内部の状態をリセット
reset();

Copilot uses AI. Check for mistakes.
Comment on lines +28 to +69
console.log('ランキングデータの読み込みを開始...');

// 1. 両方のAPIを並行して取得
const [rankingResponse, stampsResponse] = await Promise.all([
apiClient.GET('/stamps/ranking'),
apiClient.GET('/stamps'),
]);

console.log('API レスポンス - ranking:', rankingResponse);
console.log('API レスポンス - stamps:', stampsResponse);

// 2. エラーハンドリング
if (rankingResponse.error) {
throw new Error(`ランキングデータの取得に失敗しました: ${rankingResponse.error}`);
}
if (stampsResponse.error) {
throw new Error(`スタンプデータの取得に失敗しました: ${stampsResponse.error}`);
}

// 3. データの準備
const rankings = rankingResponse.data || [];
const stamps = stampsResponse.data || [];

console.log(`取得データ - rankings: ${rankings.length}件, stamps: ${stamps.length}件`);

// 4. スタンプIDをキーとしたMapを作成
const stampMap = new Map(stamps.map(stamp => [stamp.stamp_id, stamp]));

// 5. データを結合して処理済みランキングデータを生成
const processedData = rankings.map(item => ({
stamp_id: item.stamp_id,
total_count: item.total_count,
monthly_count: item.monthly_count,
stamp_name: stampMap.get(item.stamp_id)?.stamp_name ?? '不明なスタンプ',
rank: 0, // ソート時に設定
count: item.total_count, // 初期値
}));

ranking.value = processedData;
initialized.value = true;

console.log('ランキングデータの処理完了:', processedData.length, '件');
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multiple console.log statements (lines 28, 36, 37, 51, 69) are left in production code. These should either be removed or replaced with a proper logging mechanism. Console logs in production code can expose sensitive information and clutter browser consoles.

Copilot uses AI. Check for mistakes.
variant="pill"
class="w-full"
>
>
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a stray > character on this line that will cause a syntax error. This should be removed.

Suggested change
>

Copilot uses AI. Check for mistakes.
<div class="flex items-center gap-3">
<NuxtImg
:src="getFileUrl(row.original.stamp_id)"
class="m-auto w-12 h-12"
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The class m-auto should be mx-auto to match the consistent usage in line 162. Using m-auto sets margin on all sides, while mx-auto (used in the monthly ranking template) only centers horizontally, which is the correct approach for centering images.

Suggested change
class="m-auto w-12 h-12"
class="mx-auto w-12 h-12"

Copilot uses AI. Check for mistakes.

// メダル表示のマップ
const medalMap: Record<number, string> = {
1: '👑 1',
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The medalMap object should use 🥇 (first place medal) for rank 1 instead of 👑 (crown) to maintain consistency with the medal theme established by ranks 2 (🥈) and 3 (🥉). If a crown is specifically desired for first place, consider documenting this design choice in a comment.

Suggested change
1: '👑 1',
1: '🥇 1',

Copilot uses AI. Check for mistakes.
Comment on lines +41 to +44
throw new Error(`ランキングデータの取得に失敗しました: ${rankingResponse.error}`);
}
if (stampsResponse.error) {
throw new Error(`スタンプデータの取得に失敗しました: ${stampsResponse.error}`);
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error messages in lines 41 and 44 embed the raw error object in the string, which may not provide useful information to users. Consider extracting the error message or status code from the response object. For example: ${rankingResponse.error.message || 'Unknown error'} or handle specific error codes to provide more user-friendly messages.

Suggested change
throw new Error(`ランキングデータの取得に失敗しました: ${rankingResponse.error}`);
}
if (stampsResponse.error) {
throw new Error(`スタンプデータの取得に失敗しました: ${stampsResponse.error}`);
throw new Error(`ランキングデータの取得に失敗しました: ${
typeof rankingResponse.error === 'object'
? rankingResponse.error.message || JSON.stringify(rankingResponse.error)
: rankingResponse.error || 'Unknown error'
}`);
}
if (stampsResponse.error) {
throw new Error(`スタンプデータの取得に失敗しました: ${
typeof stampsResponse.error === 'object'
? stampsResponse.error.message || JSON.stringify(stampsResponse.error)
: stampsResponse.error || 'Unknown error'
}`);

Copilot uses AI. Check for mistakes.
Comment on lines +82 to +85
const refresh = async () => {
initialized.value = false;
ranking.value = [];
await loadRanking();
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The refresh function resets initialized to false and clears ranking, but if loadRanking is called when data already exists, it will return early (lines 20-22) without actually refreshing. This creates a logical inconsistency - refresh should always fetch fresh data. Consider removing the early return check in loadRanking or adding a parameter to force reload.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ランキングページの実装

3 participants