Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
ceae47f
feat: 미니게임에서 사용할 이미지,사운드 파일
hafskjfha Dec 20, 2025
0b30397
feat: 미니게임 상태 관리 및 provider 추가
hafskjfha Dec 20, 2025
75de1a9
feat: 미니게임 상수 추가
hafskjfha Dec 20, 2025
d74bdd2
feat: 미니게임 타입 파일 추가
hafskjfha Dec 20, 2025
95a0c74
feat: 미니게임 서비스 클래스 추가
hafskjfha Dec 20, 2025
05154f5
feat: 미니게임 단어 관리 함수 추가
hafskjfha Dec 20, 2025
863051d
fix: 한글 유틸 함수 저장파일 변경 및 테스트 파일 추가
hafskjfha Dec 20, 2025
0f1d890
feat: 사운드 매니저 클래스 추가
hafskjfha Dec 20, 2025
2b8768e
feat: 미니게임 매니저, 게임 로직 클래스 추가
hafskjfha Dec 20, 2025
481ec00
chore: 파일 이름 변경
hafskjfha Dec 20, 2025
55d6c16
feat: 게임 채팅 관련 훅 추가
hafskjfha Dec 20, 2025
6f6532b
feat: 게임 관련 훅 추가
hafskjfha Dec 20, 2025
66f778b
feat: 미니게임에 사용될 컴포넌트 추가
hafskjfha Dec 20, 2025
055521d
feat: 미니게임 채팅 컴포넌트 추가
hafskjfha Dec 20, 2025
5ed523a
feat: 미니게임 컴포넌트 추가
hafskjfha Dec 20, 2025
5e58464
feat: 미니게임 셋업 화면 추가
hafskjfha Dec 20, 2025
dfc6556
feat: 게임 메인 화면 컴포넌트 추가
hafskjfha Dec 20, 2025
9f5fa2b
feat: 게임 컴포넌트 및 배경 추가
hafskjfha Dec 20, 2025
26d7423
chore: 파일 이름 변경
hafskjfha Dec 20, 2025
7db26bb
feat: 미니게임 페이지 추가
hafskjfha Dec 20, 2025
78d6da7
fix: 입력창 다크모드 대응
hafskjfha Dec 20, 2025
660f67c
feat: 모바일 환경 미지원 추가
hafskjfha Dec 20, 2025
a5b707b
test: 미니게임내 컴포넌트 파일 테스트 코드 추가
hafskjfha Dec 20, 2025
80042d4
test: 미니게임내 훅 파일 테스트 코드 추가
hafskjfha Dec 20, 2025
eff3f40
test: 미니게임내 lib 파일 테스트 코드 추가
hafskjfha Dec 20, 2025
66b65a6
test: 미니게임내 서비스 파일 테스트 코드 추가
hafskjfha Dec 20, 2025
ef2e452
test: 미니게임내 주요 컴포넌트 파일 테스트 코드 추가
hafskjfha Dec 20, 2025
2c72fd2
test: 미니게임 페이지 테스트 코드 추가
hafskjfha Dec 20, 2025
4cbddff
feat: 다크모드 지원 추가
hafskjfha Dec 20, 2025
f47e057
chore: 미니게임 페이지 메타데이터 추가
hafskjfha Dec 20, 2025
76c112c
style: lint 요류 수정
hafskjfha Dec 22, 2025
0d7ed93
Merge pull request #112 from SolidLoop-studio/features/mini_game
hafskjfha Dec 22, 2025
87721ef
fix: 사운드 파일명 오타 수정
hafskjfha Dec 22, 2025
c203922
fix: 없는 단어 뒤로 가기 꼬임 문제 수정
hafskjfha Dec 22, 2025
7004ee1
chore: 서비스 제공자 이름 변경
hafskjfha Dec 22, 2025
fd2d994
fix: 단어 고급 검색 모바일 ui 수정
hafskjfha Dec 22, 2025
1e54be7
feat: 한국어 미션단어 추출A - 첫글자 미션글자 제외 옵션 추가
hafskjfha Dec 22, 2025
82768b5
fix: 스택오버플로우 버그 수정
hafskjfha Dec 22, 2025
9aa388f
chore: vercel 에널리틱스 추가
hafskjfha Dec 23, 2025
59f5d7f
feat: Open API추가 및 api docs 추가
as7ar Dec 25, 2025
30e83f3
feat: renamed following Naming Conventions
as7ar Dec 25, 2025
4d61854
feat: ㄱㄴㄷ순 정렬v4 추가
hafskjfha Dec 26, 2025
b791e38
feat: split API page
as7ar Dec 27, 2025
cfe0eff
fix: 오탈자 제거
as7ar Dec 27, 2025
ddd8f55
chore: 사용하지 않는 import 제거
hafskjfha Dec 27, 2025
d69905f
Merge branch 'dev' into ASTAR
hafskjfha Dec 27, 2025
6a55f63
feat: 한국어 끝말잇기 미션 단어 페이지 추가
hafskjfha Dec 27, 2025
95340e2
feat: 미션 탭/문서 표시시 미션 글자 하이라이팅 추가
hafskjfha Dec 27, 2025
751960d
feat: 미션 문서 최근 변경 시각 추가
hafskjfha Dec 28, 2025
1d21a84
feat: 한앞, 한쿵 미션문서 페이지 추가
hafskjfha Dec 28, 2025
a05b28b
Merge pull request #115 from SolidLoop-studio/feature/mission_word-page
hafskjfha Dec 29, 2025
c1e1595
Merge branch 'dev' into ASTAR
hafskjfha Dec 29, 2025
1055195
fix:
as7ar Jan 4, 2026
e8a2f2e
fix: lint Error
as7ar Jan 10, 2026
f2cfb46
fix: openapi link path
as7ar Jan 10, 2026
6e65769
fix: k_CanUse to k_canuse
as7ar Jan 11, 2026
cef72b3
fix: 철자 수정 누락됨
hafskjfha Jan 18, 2026
382bf6b
#113
hafskjfha Jan 18, 2026
e5e81e5
feat: api 서버 관리자 페이지 추가
hafskjfha Jan 18, 2026
9dc5c52
feat: 끄코 관련 페이지 홈 추가
hafskjfha Jan 18, 2026
21eaf12
fix: 헤더 activeIndex 수정
hafskjfha Jan 18, 2026
1219ffd
feat: 끄코 유저 정보 조회 페이지 추가
hafskjfha Jan 18, 2026
cb79044
feat: 끄코 api, 이미지 사용을 위한 프록시 추가
hafskjfha Jan 18, 2026
b826fbd
feat: 경험치 랭킹, 레벨 아이콘 추가, 타입 파일 분리
hafskjfha Jan 18, 2026
a365d04
fix: 상수 파일 분리, 슬롯 이름 지정
hafskjfha Jan 18, 2026
3a72247
feat: 유저 프로필 이미지 로드 추가
hafskjfha Jan 19, 2026
dfecc15
feat: 유저 장착 아이템 이미지 추가
hafskjfha Jan 19, 2026
134e458
fix: 부동 소수점 오류 수정
hafskjfha Jan 19, 2026
7592cd9
feat: 배지 표시추가, 프로필 이미지 default layer 추가
hafskjfha Jan 20, 2026
e65dab6
chore: 이미지 프록시 런타임 명시
hafskjfha Jan 20, 2026
83d448b
feat: 최근 검색어 표시, 주의 문구 추가
hafskjfha Jan 20, 2026
74427da
fix: 특이 옵션 처리 추가
hafskjfha Jan 20, 2026
4f8a379
refactor: 관심사 분리 원칙에 맞게 리펙토링
hafskjfha Jan 20, 2026
7c536a8
chore: lint 에러 수정
hafskjfha Jan 20, 2026
ed94114
test: 끄코 유저 정보 페이지 테스트 코드 추가
hafskjfha Jan 20, 2026
1533f63
fix: 양손에 같은 아이템을 가지고 있을때 미표시되는 버그 수정
hafskjfha Jan 20, 2026
a3d732e
fix: 이전 검색어 표시 로직 수정, 검색후 UX 개선
hafskjfha Jan 20, 2026
34491b8
fix: 의견 반영
hafskjfha Jan 20, 2026
ca1693a
feat: 끄코 랭킹 페이지 구현
hafskjfha Jan 20, 2026
9d70dd3
feat: 랭킹창 아바타 추가
hafskjfha Jan 21, 2026
fe2cd14
test: kkuko 페이지 테스트 코드 작성
hafskjfha Jan 21, 2026
2e3e35a
refactor: hook 분리
hafskjfha Jan 21, 2026
45897e0
feat: 에러 핸들링 강화
hafskjfha Jan 21, 2026
746483b
fix: suspense 추가
hafskjfha Jan 22, 2026
cc48a8a
feat: 크롤러 재시작 버튼 추가
hafskjfha Jan 22, 2026
c84dfef
fix: 문서 필터링 입력창에 특문 입력시 발생하는 에러 수정
hafskjfha Jan 22, 2026
2e79c24
feat: 자퀴 주제 선택창 자음 검색 추가
hafskjfha Jan 22, 2026
2d87586
feat: ㄱㄴㄷ순 정렬 v4 추가
hafskjfha Jan 22, 2026
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
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
/.pnp
.pnp.*
.yarn/*
/.next
!.yarn/patches
!.yarn/plugins
!.yarn/releases
Expand Down Expand Up @@ -42,4 +43,7 @@ next-env.d.ts

# Test
app/test/
app/api/test
app/api/test

# Idea
/.idea
6 changes: 5 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,9 @@
"http",
"net"
],
"discord.enabled": true
"discord.enabled": true,
"chat.tools.terminal.autoApprove": {
"npx jest": true,
"npm run lint": true
}
}
31 changes: 31 additions & 0 deletions __tests__/kkuko/KkukoHome.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { render, screen } from '@testing-library/react';
import KkukoHome from '@/app/kkuko/KkukoHome'; // Adjust import path if needed

describe('KkukoHome', () => {
it('should render the title and description', () => {
render(<KkukoHome />);

expect(screen.getByText('끄코 정보')).toBeInTheDocument();
expect(screen.getByText('끄투코리아의 유저정보와 랭킹을 조회 할 수 있습니다.')).toBeInTheDocument();
});

it('should render the Profile section with link', () => {
render(<KkukoHome />);

expect(screen.getByRole('heading', { name: '프로필' })).toBeInTheDocument();
expect(screen.getByText('끄투코리아의 유저 정보와 전적 등을 확인할 수 있습니다.')).toBeInTheDocument();

const profileLink = screen.getByRole('link', { name: /둘러보기/i });
expect(profileLink).toHaveAttribute('href', '/kkuko/profile');
});

it('should render the Ranking section with link', () => {
render(<KkukoHome />);

expect(screen.getByRole('heading', { name: '랭킹' })).toBeInTheDocument();
expect(screen.getByText('각 모드별로 승리가 많은 유저들의 랭킹을 확인할 수 있습니다.')).toBeInTheDocument();

const rankingLink = screen.getByRole('link', { name: /구경하기/i });
expect(rankingLink).toHaveAttribute('href', '/kkuko/ranking');
});
});
54 changes: 54 additions & 0 deletions __tests__/kkuko/profile/components/ItemModal.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import ItemModal from '@/app/kkuko/profile/components/ItemModal';
import { ItemInfo, ProfileData } from '@/app/types/kkuko.types';

jest.mock('@/app/kkuko/shared/components/TryRenderImg', () => () => <div data-testid="item-img" />);
jest.mock('@/app/kkuko/profile/utils/profileHelper', () => ({
getSlotName: (slot: string) => slot,
extractColorFromLabel: () => [],
parseDescription: (desc: string) => [{ text: desc, colorKey: null }],
getOptionName: (key: string) => key,
formatNumber: (num: number) => num.toString(),
}));
jest.mock('@/app/kkuko/shared/lib/const', () => ({
NICKNAME_COLORS: {},
}));

describe('ItemModal', () => {
const mockProfileData: ProfileData = {
equipment: [
{ itemId: 'item1', slot: 'head' },
],
} as any;

const mockItemsData: ItemInfo[] = [
{
id: 'item1',
name: 'Cool Hat',
image: 'hat.png',
desc: 'A nice hat',
options: { score: 10 }
},
] as any;

it('should render items in modal', () => {
const onClose = jest.fn();
render(<ItemModal itemsData={mockItemsData} profileData={mockProfileData} onClose={onClose} />);

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

it('should call onClose when close button is clicked', () => {
const onClose = jest.fn();
render(<ItemModal itemsData={mockItemsData} profileData={mockProfileData} onClose={onClose} />);

const closeButtons = screen.getAllByRole('button');
// Usually the first one or the "X" button.
fireEvent.click(closeButtons[0]);
expect(onClose).toHaveBeenCalled();
});
});
54 changes: 54 additions & 0 deletions __tests__/kkuko/profile/components/ProfileHeader.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import ProfileHeader from '@/app/kkuko/profile/components/ProfileHeader';
import { ProfileData, ItemInfo } from '@/types/kkuko.types';
import * as profileHelper from '@/app/kkuko/profile/utils/profileHelper';

jest.mock('@/app/kkuko/profile/utils/profileHelper');
jest.mock('@/app/kkuko/shared/components/ProfileAvatar', () => () => <div data-testid="profile-avatar" />);
jest.mock('@/app/kkuko/shared/components/TryRenderImg', () => (props: any) => <img alt={props.alt} src={props.url} />);

describe('ProfileHeader', () => {
const mockProfileData: ProfileData = {
user: {
id: 'test_user',
nickname: 'Test Nick',
level: 10,
exp: 1000,
exordial: 'Hello World'
},
equipment: [
{ id: 1, itemId: 'badge1', slot: 'pbdg', stat: {} }
],
presence: { updatedAt: new Date().toISOString() },
game: {
win: 10, lose: 5, draw: 0,
max_combo: 100, max_score: 50000
}
} as any; // Partial mock

const mockItemsData: ItemInfo[] = [
{ id: 'badge1', name: 'Best Badge', description: '', group: 'pbdg', options: {}, updatedAt: 1 }
];

beforeEach(() => {
(profileHelper.getNicknameColor as jest.Mock).mockReturnValue('#000000');
(profileHelper.formatLastSeen as jest.Mock).mockReturnValue('1분 전');
});

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

expect(screen.getByText('Test Nick')).toBeInTheDocument();
expect(screen.getByText('Hello World')).toBeInTheDocument();
expect(screen.getByText('ID: test_user')).toBeInTheDocument();
expect(screen.getByText('Lv. 10')).toBeInTheDocument();
expect(screen.getByText('경험치 랭킹: #1')).toBeInTheDocument();
});

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

expect(screen.getByAltText('Best Badge')).toBeInTheDocument();
});
});
39 changes: 39 additions & 0 deletions __tests__/kkuko/profile/components/ProfileRecords.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import ProfileRecords from '@/app/kkuko/profile/components/ProfileRecords';
import { Mode } from '@/app/types/kkuko.types';

// Mock helper functions
jest.mock('@/app/kkuko/profile/utils/profileHelper', () => ({
groupRecordsByMode: jest.fn(() => ({
kor: [
{ id: '1', modeId: 'kr_word', total: 10, win: 5, exp: 100 },
],
eng: [],
})),
getModeName: jest.fn(() => '한국어 끝말잇기'),
calculateWinRate: jest.fn(() => '50.0'),
}));

describe('ProfileRecords', () => {
const mockModesData: Mode[] = [
{ id: 'kr_word', name: '한국어 끝말잇기', category: 'kor' },
];
const mockProfileData: any = {
record: [
{ id: '1', modeId: 'kr_word', total: 10, win: 5, exp: 100 },
],
};

it('should render records correctly', () => {
render(<ProfileRecords profileData={mockProfileData} modesData={mockModesData} />);

expect(screen.getByText('전적')).toBeInTheDocument();
expect(screen.getByText('한국어')).toBeInTheDocument();
expect(screen.getByText('한국어 끝말잇기')).toBeInTheDocument();
expect(screen.getByText('10')).toBeInTheDocument(); // total
expect(screen.getByText('5')).toBeInTheDocument(); // win
expect(screen.getByText('50.0%')).toBeInTheDocument(); // rate
expect(screen.getByText('100')).toBeInTheDocument(); // exp
});
});
72 changes: 72 additions & 0 deletions __tests__/kkuko/profile/components/ProfileSearch.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import ProfileSearch from '@/app/kkuko/profile/components/ProfileSearch';
import { useRouter, useSearchParams } from 'next/navigation';

jest.mock('next/navigation', () => ({
useRouter: jest.fn(),
useSearchParams: jest.fn(),
}));

describe('ProfileSearch', () => {
const mockRouter = { push: jest.fn(), replace: jest.fn() };
const mockSearchParams = { get: jest.fn() };
const mockOnRemoveRecentSearch = jest.fn();
const mockOnSearch = jest.fn();

beforeEach(() => {
(useRouter as jest.Mock).mockReturnValue(mockRouter);
(useSearchParams as jest.Mock).mockReturnValue(mockSearchParams);
mockRouter.push.mockClear();
mockRouter.replace.mockClear();
mockSearchParams.get.mockClear();
mockOnRemoveRecentSearch.mockClear();
mockOnSearch.mockClear();
});

it('should render search input and button', () => {
const { container } = render(<ProfileSearch loading={false} recentSearches={[]} onRemoveRecentSearch={mockOnRemoveRecentSearch} onSearch={mockOnSearch} />);
expect(screen.getByPlaceholderText('유저 검색...')).toBeInTheDocument();
// search button check
const button = container.querySelector('button.bg-blue-500');
expect(button).toBeInTheDocument();
});

it('should change search type', () => {
render(<ProfileSearch loading={false} recentSearches={[]} onRemoveRecentSearch={mockOnRemoveRecentSearch} onSearch={mockOnSearch} />);
// By default 'nick' is selected (label usually '닉네임')
// const select = screen.getByRole('combobox'); // If it is a select
// Or buttons if it's a toggle.
// Let's assume there is a select element based on typical UI or check code more deeply if needed.
// Reading code... `const [searchType, setSearchType] = useState<'nick' | 'id'>('nick');`
// It likely uses a select dropdown.
const select = screen.getByRole('combobox');
fireEvent.change(select, { target: { value: 'id' } });
expect(select).toHaveValue('id');
});

it('should submit search', () => {
const { container } = render(<ProfileSearch loading={false} recentSearches={[]} onRemoveRecentSearch={mockOnRemoveRecentSearch} onSearch={mockOnSearch} />);
const input = screen.getByPlaceholderText('유저 검색...');
fireEvent.change(input, { target: { value: 'testuser' } });

const button = container.querySelector('button.bg-blue-500');
if (button) {
fireEvent.click(button);
} else {
throw new Error('Search button not found');
}

expect(mockOnSearch).toHaveBeenCalledWith('testuser', 'nick');
});

it('should show recent searches on focus', () => {
const recentSearches = [{ query: 'past', type: 'nick', timestamp: 123 } as any];
render(<ProfileSearch loading={false} recentSearches={recentSearches} onRemoveRecentSearch={mockOnRemoveRecentSearch} onSearch={mockOnSearch} />);

const input = screen.getByPlaceholderText('유저 검색...');
fireEvent.focus(input);

expect(screen.getByText('past')).toBeInTheDocument();
});
});
44 changes: 44 additions & 0 deletions __tests__/kkuko/profile/components/ProfileStats.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import ProfileStats from '@/app/kkuko/profile/components/ProfileStats';
import { ItemInfo } from '@/types/kkuko.types';
import * as profileHelper from '@/app/kkuko/profile/utils/profileHelper';

jest.mock('@/app/kkuko/profile/utils/profileHelper');

describe('ProfileStats', () => {
const mockItemsData: ItemInfo[] = [
{ id: '1', name: 'Item 1', description: '', group: '', options: { str: 10 }, updatedAt: 1 }
];
const mockOnShowDetail = jest.fn();

beforeEach(() => {
(profileHelper.calculateTotalOptions as jest.Mock).mockReturnValue({
'str': 10000,
'gEXP': 5000
});
(profileHelper.getOptionName as jest.Mock).mockImplementation((key) => key);
(profileHelper.formatNumber as jest.Mock).mockImplementation((val) => (val / 1000).toString());
});

it('should render total options correctly', () => {
render(<ProfileStats itemsData={mockItemsData} onShowDetail={mockOnShowDetail} />);

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

// gEXP: 5000 -> +5%p
expect(screen.getByText('gEXP')).toBeInTheDocument();
expect(screen.getByText('+5%p')).toBeInTheDocument();
});

it('should call onShowDetail when button clicked', () => {
render(<ProfileStats itemsData={mockItemsData} onShowDetail={mockOnShowDetail} />);

const button = screen.getByRole('button', { name: '보기' });
fireEvent.click(button);

expect(mockOnShowDetail).toHaveBeenCalled();
});
});
Loading