Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions src/__tests__/kkuko/profile/components/ItemModal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ jest.mock('@/app/kkuko/profile/utils/profileHelper', () => ({
}));
jest.mock('@/app/kkuko/shared/lib/const', () => ({
NICKNAME_COLORS: {},
OPTION_NAMES: { gEXP: '획득 경험치' }
}));

describe('ItemModal', () => {
Expand All @@ -28,7 +29,7 @@ describe('ItemModal', () => {
name: 'Cool Hat',
image: 'hat.png',
desc: 'A nice hat',
options: { score: 10 }
options: { gEXP: 0.1 }
},
] as any;

Expand All @@ -38,8 +39,8 @@ describe('ItemModal', () => {

expect(screen.getByText('장착 아이템 목록')).toBeInTheDocument();
expect(screen.getByText('Cool Hat')).toBeInTheDocument();
expect(screen.getByText('score:')).toBeInTheDocument();
expect(screen.getByText('+10')).toBeInTheDocument();
expect(screen.getByText('gEXP:')).toBeInTheDocument();
expect(screen.getByText('+10%p')).toBeInTheDocument();
});

it('should call onClose when close button is clicked', () => {
Expand Down
4 changes: 2 additions & 2 deletions src/__tests__/kkuko/profile/components/ProfileHeader.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ describe('ProfileHeader', () => {
});

it('should render user info', () => {
render(<ProfileHeader profileData={mockProfileData} itemsData={mockItemsData} expRank={1} />);
render(<ProfileHeader profileData={mockProfileData} itemsData={mockItemsData} expRank={1} onRefreshRequest={() => {}} isRefreshing={false} />);

expect(screen.getByText('Test Nick')).toBeInTheDocument();
expect(screen.getByText('Hello World')).toBeInTheDocument();
Expand All @@ -47,7 +47,7 @@ describe('ProfileHeader', () => {
});

it('should render badges', () => {
render(<ProfileHeader profileData={mockProfileData} itemsData={mockItemsData} expRank={null} />);
render(<ProfileHeader profileData={mockProfileData} itemsData={mockItemsData} expRank={null} onRefreshRequest={() => {}} isRefreshing={false} />);

expect(screen.getByAltText('Best Badge')).toBeInTheDocument();
});
Expand Down
6 changes: 3 additions & 3 deletions src/__tests__/kkuko/profile/components/ProfileStats.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ describe('ProfileStats', () => {

beforeEach(() => {
(profileHelper.calculateTotalOptions as jest.Mock).mockReturnValue({
'str': 10000,
'hEXP': 10000,
'gEXP': 5000
});
(profileHelper.getOptionName as jest.Mock).mockImplementation((key) => key);
Expand All @@ -24,8 +24,8 @@ describe('ProfileStats', () => {
it('should render total options correctly', () => {
render(<ProfileStats itemsData={mockItemsData} onShowDetail={mockOnShowDetail} />);

// STR: 10000 -> +10
expect(screen.getByText('str')).toBeInTheDocument();
// hEXP: 10000 -> +10
expect(screen.getByText('hEXP')).toBeInTheDocument();
expect(screen.getByText('+10')).toBeInTheDocument();

// gEXP: 5000 -> +5%p
Expand Down
22 changes: 11 additions & 11 deletions src/__tests__/kkuko/profile/utils/profileHelper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,13 @@ describe('profileHelper', () => {
describe('calculateTotalOptions', () => {
it('should calculate totals correctly for normal options', () => {
const items: ItemInfo[] = [
{ id: '1', name: 'Item1', description: '', group: '', options: { str: 1, dex: 2 }, updatedAt: 1 },
{ id: '2', name: 'Item2', description: '', group: '', options: { str: 3 }, updatedAt: 1 }
{ id: '1', name: 'Item1', description: '', group: '', options: { gEXP: 0.01, dex: 2 }, updatedAt: 1 },
{ id: '2', name: 'Item2', description: '', group: '', options: { gEXP: 0.03 }, updatedAt: 1 }
];
const result = calculateTotalOptions(items);
// 1 * 1 + 3 * 1 = 4
expect(result['str']).toBe(4);
expect(result['dex']).toBe(2);
// 0.01 + 0.03 = 0.04 -> formatted to 4
expect(result['gEXP']).toBe(4);
expect(result['dex']).toBe(undefined);
});

it('should handle special options', () => {
Expand All @@ -88,14 +88,14 @@ describe('profileHelper', () => {
id: '3', name: 'Special', description: '', group: '',
options: {
date: now - 10000, // Past
before: { str: 1 },
after: { str: 5 }
before: { gEXP: 0.01 },
after: { gEXP: 0.05 }
}, updatedAt: 1
}
];
const result = calculateTotalOptions(items);
// Should use 'after'
expect(result['str']).toBe(5);
expect(result['gEXP']).toBe(5);
});

it('should handle special options (before)', () => {
Expand All @@ -105,14 +105,14 @@ describe('profileHelper', () => {
id: '3', name: 'Special', description: '', group: '',
options: {
date: now + 10000, // Future
before: { str: 1 },
after: { str: 5 }
before: { gEXP: 0.01 },
after: { gEXP: 0.05 }
}, updatedAt: 1
}
];
const result = calculateTotalOptions(items);
// Should use 'before'
expect(result['str']).toBe(1);
expect(result['gEXP']).toBe(1);
});
});

Expand Down
48 changes: 27 additions & 21 deletions src/app/api/kkuko/api/ranking/route.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,42 @@
import { NextRequest, NextResponse } from 'next/server';
import axios from 'axios';

type RankingResponse = {
target: string;
data: {
id: string,
rank: number,
score: number,
nick: string
diff: string
}[]
id: string;
rank: number;
score: number;
nick: string;
diff: string;
}[];
};

export async function GET(request: NextRequest){
export async function GET(request: NextRequest) {
try {
const id = request.nextUrl.searchParams.get('id');
if (!id || isNaN(Number(id))){
return NextResponse.json(
{ error: 'Invalid ranking ID' },
{ status: 400 }
);

if (!id || isNaN(Number(id))) {
return NextResponse.json({ error: 'Invalid ranking ID' }, { status: 400 });
}
const res = await axios.get<RankingResponse>(`https://kkutu.co.kr/o/ranking?id=${id}`);
const rankingData = res.data.data.find(user => user.id === id);

const res = await fetch(`https://kkutu.co.kr/o/ranking?id=${id}`, {
next: { revalidate: 300 }
});

if (!res.ok) throw new Error('Network response was not ok');

const data: RankingResponse = await res.json();
const rankingData = data.data.find(user => user.id === id);

if (!rankingData) {
return NextResponse.json(
{ error: 'User not found in ranking data' },
{ status: 404 }
);
} else {
return NextResponse.json({rank: rankingData.rank + 1, id: rankingData.id});
return NextResponse.json({ error: 'User not found' }, { status: 404 });
}

return NextResponse.json({
rank: rankingData.rank + 1,
id: rankingData.id
});

} catch {
return NextResponse.json(
{ error: 'Failed to fetch ranking data' },
Expand Down
30 changes: 1 addition & 29 deletions src/app/kkuko/profile/components/ProfileHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Link from 'next/link';
import { ItemInfo, ProfileData } from '@/src/app/types/kkuko.types';
import TryRenderImg from '../../shared/components/TryRenderImg';
import ProfileAvatar from '../../shared/components/ProfileAvatar';
import { formatLastSeen, formatObservedAt, getNicknameStyle } from '../utils/profileHelper';
import { formatObservedAt, getNicknameStyle } from '../utils/profileHelper';

interface ProfileHeaderProps {
profileData: ProfileData;
Expand Down Expand Up @@ -117,34 +117,6 @@ export default function ProfileHeader({
<p className="text-lg font-semibold text-gray-900 dark:text-gray-100">{formatObservedAt(profileData.user.observedAt)}</p>
</div>

<div>
<p className="text-sm text-gray-500 dark:text-gray-400">접속 상태</p>
<p className="text-lg font-semibold">
{profileData.presence.channelId ? (
<span className="text-green-600 dark:text-green-400">온라인</span>
) : (
<span className="text-gray-500 dark:text-gray-400">오프라인</span>
)}
</p>
{profileData.presence.channelId ? (
<p className="text-xs text-gray-600 dark:text-gray-400">채널: {profileData.presence.channelId}</p>
) : (
profileData.presence.updatedAt !== null ?
<p className="text-xs text-gray-600 dark:text-gray-400">마지막 접속: {formatLastSeen(profileData.presence.updatedAt)}</p> :
null
)}
</div>

<div>
<p className="text-sm text-gray-500 dark:text-gray-400">방 정보</p>
<p className="text-lg font-semibold text-gray-900 dark:text-gray-100">
{profileData.presence.roomId ? (
<span>방 {profileData.presence.roomId}</span>
) : (
<span className="text-gray-400 dark:text-gray-500">미입장</span>
)}
</p>
</div>
</div>
<div className="flex justify-end mt-1 items-center gap-3">
<button
Expand Down
15 changes: 10 additions & 5 deletions src/app/kkuko/shared/components/TryRenderImg.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,11 @@ export default function TryRenderImg({
}: Props) {
const [attempt, setAttempt] = useState(0);
const [failed, setFailed] = useState(false);
const [src, setSrc] = useState(url);

const isCdn = url.startsWith('https://cdn.kkutu.co.kr/img/');
const [src, setSrc] = useState(`${isCdn ? 'https://img-proxy.jtw7913.workers.dev?v=2&url=' : ''}${url}`);

useEffect(() => {
setSrc(url);
setSrc(`${isCdn ? 'https://img-proxy.jtw7913.workers.dev?v=2&url=' : ''}${url}`);
setAttempt(0);
setFailed(false);
}, [url]);
Expand All @@ -40,8 +41,8 @@ export default function TryRenderImg({
if (attempt < maxRetries) {
const next = attempt + 1;
setAttempt(next);
const separator = url.includes("?") ? "&" : "?";
setSrc(`${url}${separator}r=${next}&ts=${Date.now()}`);
const separator = (url.includes("?") || isCdn) ? "&" : "?";
setSrc(`${isCdn ? 'https://img-proxy.jtw7913.workers.dev?v=2&url=' : ''}${url}${separator}r=${next}&ts=${Date.now()}`);
} else {
setFailed(true);
onFailure?.();
Expand All @@ -66,6 +67,8 @@ export default function TryRenderImg({
className={className}
onError={handleError}
onLoad={onLoad}
unoptimized={isCdn}
crossOrigin={isCdn ? "anonymous" : undefined}
/>
) : (
<div style={{ position: "relative" }} className={className}>
Expand All @@ -77,6 +80,8 @@ export default function TryRenderImg({
style={{ objectFit: "cover" }}
onError={handleError}
onLoad={onLoad}
unoptimized={isCdn}
crossOrigin={isCdn ? "anonymous" : undefined}
/>
</div>
)}
Expand Down
Loading