Skip to content

Conversation

@hafskjfha
Copy link
Collaborator

Resolved #114

@hafskjfha hafskjfha linked an issue Dec 29, 2025 that may be closed by this pull request
4 tasks
@vercel
Copy link
Contributor

vercel bot commented Dec 29, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
kkuko-utils Ready Ready Preview, Comment Dec 29, 2025 1:12am

@hafskjfha hafskjfha requested a review from Copilot December 29, 2025 01:11
@hafskjfha hafskjfha changed the base branch from main to dev December 29, 2025 01:11
@github-actions
Copy link
Contributor

Test Results

256 tests   256 ✅  45s ⏱️
 38 suites    0 💤
  1 files      0 ❌

Results for commit 1d21a84.

@github-actions
Copy link
Contributor

🧪 Test Results & Coverage Report

✅ All Tests Passed! (256/256)

🎉 Great work! All your tests are passing.

📋 Test Suites Summary

Test Suite Tests Status Duration
jest tests 256/256 22787ms

🔍 Detailed Test Results

jest tests (256/256 passed, 22787ms)

✅ KoreanMission 초기 렌더링이 정상적으로 되는지 확인 (88ms)
✅ KoreanMission 1미 포함 체크박스가 정상적으로 동작하는지 확인 (195ms)
✅ KoreanMission 미션 글자 표시 체크박스가 정상적으로 동작하는지 확인 (62ms)
✅ KoreanMission 정렬 모드 체크박스가 정상적으로 동작하는지 확인 (75ms)
✅ KoreanMission 파일 내용이 없을 때 단어 추출 버튼이 비활성화되는지 확인 (31ms)
✅ KoreanMission 파일 업로드 후 단어 추출이 정상적으로 동작하는지 확인 (157ms)
✅ KoreanMission 단어 추출 결과에 따라 다운로드 버튼이 활성화되는지 확인 (158ms)
✅ KoreanMission 1미 포함 옵션이 제대로 적용되는지 확인 (204ms)
✅ KoreanMission 다운로드 기능이 정상적으로 동작하는지 확인 (182ms)

📋 View detailed workflow results

📊 Code Coverage Report

Metric Coverage Status
Lines 20.97% (1827/8709) 🔴 Poor
Statements 20.41% (1979/9693) 🔴 Poor
Functions 17.38% (346/1990) 🔴 Poor
Branches 15.82% (754/4766) 🔴 Poor

🔴 Low Coverage: 18.6%

Your code coverage is below recommended levels. Please add more tests.

📂 Coverage by File (176 files tested)

Click to expand file-by-file coverage
File Lines Functions Branches Statements
...ils/app/components/ui/radio-group.tsx 🟢 100% 🟢 100% 🟢 100% 🟢 100%
...ko-utils/kkuko-utils/app/lib/utils.ts 🟢 100% 🟢 100% 🟢 100% 🟢 100%
...s/app/mini-game/MobileUnsupported.tsx 🟢 100% 🟢 100% 🟢 100% 🟢 100%
...uko-utils/app/mini-game/game/Game.tsx 🟢 100% 🟢 100% 🟢 100% 🟢 100%
...-utils/app/mini-game/game/GameBox.tsx 🟢 100% 🟢 100% 🟢 100% 🟢 100%
...utils/app/mini-game/game/GameChat.tsx 🟢 100% 🟢 100% 🟡 86.66% 🟢 100%
...uko-utils/app/mini-game/game/const.ts 🟢 100% 🟢 100% 🟢 100% 🟢 100%
...game/game/components/ConfirmModal.tsx 🟢 100% 🟢 100% 🟢 100% 🟢 100%
...ni-game/game/components/GameInput.tsx 🟢 100% 🟢 100% 🟢 100% 🟢 100%
...e/game/components/GameResultModal.tsx 🟢 100% 🟢 100% 🟢 100% 🟢 100%
...ini-game/game/components/GraphBar.tsx 🟢 100% 🟢 100% 🟡 80% 🟢 100%
...ni-game/game/components/HelpModal.tsx 🟢 100% 🟢 100% 🟢 100% 🟢 100%
...me/game/components/StartCharModal.tsx 🟢 100% 🟢 100% 🟢 100% 🟢 100%
...pp/mini-game/game/lib/SoundManager.ts 🟢 100% 🟢 100% 🟠 72.72% 🟢 97.5%
...s/app/mini-game/game/lib/fileUtils.ts 🟢 100% 🟢 100% 🟢 100% 🟢 100%
... and 161 more files

📈 Recommendations

Consider improving test coverage for:

  • 📄 AutoLogin.tsx (0% lines covered)
  • 📄 ErrorPage.tsx (0% lines covered)
  • 📄 Home.tsx (0% lines covered)

🤖 Automated report | ⏱️ Generated: 2025. 12. 29. 오전 10:12:18 KST | 🔄 Workflow: Run Tests on main PR

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 implements a mission word page feature that introduces special document pages for displaying Korean mission characters. The feature adds the ability to view mission words organized by specific Korean characters (가, 나, 다, etc.) with three different page types: first-letter mission words, last-letter mission words, and 3-character mission words.

Key Changes

  • Added database support for mission word marking with new mission_mark field in the words table
  • Implemented mission character filtering and highlighting in word lists
  • Created special index pages (IDs 208, 223, 238) that display grids linking to individual character pages
  • Added logic to fetch and display mission words based on character masks using new database functions

Reviewed changes

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

Show a summary per file
File Description
supabase/.temp/cli-latest Updated Supabase CLI version from v2.62.10 to v2.67.1
app/types/database.types.ts Added mission_mark field to words table schema and new RPC functions for mission word queries
app/lib/lib.ts Added misssionCharMask utility function to generate bit masks for mission characters
app/lib/supabase/SupabaseClientManager.ts Implemented mission word filtering logic for special document IDs (209-252) and added docsLastUpdate method
app/lib/supabase/ISupabaseClientManager.ts Updated interface types to include optional mission_mark field and new docsLastUpdate method
app/words-docs/[id]/WordsTableBody.tsx Added isSp parameter for special mission character highlighting
app/words-docs/[id]/Table.tsx Implemented character highlighting in word cells for mission character display
app/words-docs/[id]/DocsDataPage.tsx Added special handling for index pages (208, 223, 238) that return empty word data
app/words-docs/[id]/DocsDataHome.tsx Added rendering for special index pages showing character grid with last update times, and fetching logic for character update times

}
} else if (239 <= name && name <= 252) {
const c = ['가', '나', '다', '라', '마', '바', '사', '아', '자', '차', '카', '타', '파', '하'][name - 239];
const bit = misssionCharMask([c]);
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

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

The function call contains a typo in the function name: "misssionCharMask" should be "missionCharMask" (three 's' instead of two).

Copilot uses AI. Check for mistakes.
Comment on lines +215 to +318
} else if (209 <= name && name <= 222) {
const c = ['가', '나', '다', '라', '마', '바', '사', '아', '자', '차', '카', '타', '파', '하'][name - 209];
const bit = misssionCharMask([c]);
if (bit !== 0) {
const { data: missionKWordsData, error: missionKWordsError } = await this.supabase.rpc('get_mission_words', { target_mask: bit })
if (missionKWordsError) return { data: null, error: missionKWordsError }

const grouped: Record<string, NonNullable<typeof missionKWordsData>> = {};
(missionKWordsData || []).forEach(item => {
const f = item.word[0];
if (!grouped[f]) grouped[f] = [];
grouped[f].push(item);
});

const filteredWords: NonNullable<typeof missionKWordsData> = [];
Object.values(grouped).forEach(group => {
const multi = group.filter(w => w.word.split(c).length - 1 >= 2);
const single = group.filter(w => w.word.split(c).length - 1 === 1);

filteredWords.push(...multi);

if (multi.length < 10) {
const needed = 10 - multi.length;
single.sort((a, b) => {
const lenDiff = b.word.length - a.word.length;
if (lenDiff !== 0) return lenDiff;
return a.word.localeCompare(b.word);
});
filteredWords.push(...single.slice(0, needed));
}
});

return { data: { words: filteredWords, waitWords: [] }, error: null }
}
}
else if (224 <= name && name <= 237) {
const c = ['가', '나', '다', '라', '마', '바', '사', '아', '자', '차', '카', '타', '파', '하'][name - 224];
const bit = misssionCharMask([c]);
if (bit !== 0) {
const { data: missionKWordsData, error: missionKWordsError } = await this.supabase.rpc('get_mission_words', { target_mask: bit })
if (missionKWordsError) return { data: null, error: missionKWordsError }

const grouped: Record<string, NonNullable<typeof missionKWordsData>> = {};
(missionKWordsData || []).forEach(item => {
const f = item.word[item.word.length - 1];
if (!grouped[f]) grouped[f] = [];
grouped[f].push(item);
});

const filteredWords: NonNullable<typeof missionKWordsData> = [];
Object.values(grouped).forEach(group => {
const multi = group.filter(w => w.word.split(c).length - 1 >= 2);
const single = group.filter(w => w.word.split(c).length - 1 === 1);

filteredWords.push(...multi);

if (multi.length < 10) {
const needed = 10 - multi.length;
single.sort((a, b) => {
const lenDiff = b.word.length - a.word.length;
if (lenDiff !== 0) return lenDiff;
return a.word.localeCompare(b.word);
});
filteredWords.push(...single.slice(0, needed));
}
});

return { data: { words: filteredWords, waitWords: [] }, error: null }

}
} else if (239 <= name && name <= 252) {
const c = ['가', '나', '다', '라', '마', '바', '사', '아', '자', '차', '카', '타', '파', '하'][name - 239];
const bit = misssionCharMask([c]);
if (bit !== 0) {
const { data: missionKWordsData, error: missionKWordsError } = await this.supabase.rpc('get_mission_len3_words', { target_mask: bit })
if (missionKWordsError) return { data: null, error: missionKWordsError }

const grouped: Record<string, NonNullable<typeof missionKWordsData>> = {};
(missionKWordsData || []).forEach(item => {
const f = item.word[0];
if (!grouped[f]) grouped[f] = [];
grouped[f].push(item);
});

const filteredWords: NonNullable<typeof missionKWordsData> = [];
Object.values(grouped).forEach(group => {
const multi = group.filter(w => w.word.split(c).length - 1 >= 2);
const single = group.filter(w => w.word.split(c).length - 1 === 1);

filteredWords.push(...multi);

if (multi.length < 10) {
const needed = 10 - multi.length;
single.sort((a, b) => {
const lenDiff = b.word.length - a.word.length;
if (lenDiff !== 0) return lenDiff;
return a.word.localeCompare(b.word);
});
filteredWords.push(...single.slice(0, needed));
}
});

return { data: { words: filteredWords, waitWords: [] }, error: null }
}
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

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

This code block is duplicated three times (lines 215-248, 250-283, and 285-318) with only minor variations. The logic for fetching and filtering mission words is identical except for the character index calculation (name - 209, name - 224, name - 239) and the grouping field (word[0], word[word.length - 1], word[0]). Consider extracting this into a reusable helper function that accepts the character, the RPC function name, and the grouping strategy as parameters to improve maintainability and reduce code duplication.

Copilot uses AI. Check for mistakes.
Comment on lines +41 to +47
if (id === 208 || id === 223 || id === 238) {
const p = {title: docsData.name, lastUpdate: docsData.last_update, typez: docsData.typez}
setWordsData({words: [], metadata: p, starCount:docsStarData.map(({user_id})=>user_id)});
await SCM.update().docView(docsData.id);
updateLoadingState(100, "완료!");
return;
}
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

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

The special ID ranges and magic numbers (208, 223, 238, and the range conditions) are duplicated here and in other parts of the file. Consider defining these as named constants (e.g., MISSION_WORD_INDEX_IDS, MISSION_WORD_FIRST_RANGE, MISSION_WORD_LAST_RANGE, MISSION_WORD_LEN3_RANGE) to improve maintainability and reduce the chance of inconsistencies.

Copilot uses AI. Check for mistakes.
const [activeTab, setActiveTab] = useState<TabType>("all");
const user = useSelector((state: RootState) => state.user);
const user = useSelector((state: RootState) => state.user);
const specialIds = [208, 223, 238];
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

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

The specialIds array is defined with specific values [208, 223, 238], but these same values are checked multiple times throughout the file and in DocsDataPage.tsx. This creates a maintenance burden where changes need to be synchronized across multiple locations. Consider defining this as a shared constant at module level or in a configuration file that can be imported where needed.

Copilot uses AI. Check for mistakes.
Comment on lines +215 to +318
} else if (209 <= name && name <= 222) {
const c = ['가', '나', '다', '라', '마', '바', '사', '아', '자', '차', '카', '타', '파', '하'][name - 209];
const bit = misssionCharMask([c]);
if (bit !== 0) {
const { data: missionKWordsData, error: missionKWordsError } = await this.supabase.rpc('get_mission_words', { target_mask: bit })
if (missionKWordsError) return { data: null, error: missionKWordsError }

const grouped: Record<string, NonNullable<typeof missionKWordsData>> = {};
(missionKWordsData || []).forEach(item => {
const f = item.word[0];
if (!grouped[f]) grouped[f] = [];
grouped[f].push(item);
});

const filteredWords: NonNullable<typeof missionKWordsData> = [];
Object.values(grouped).forEach(group => {
const multi = group.filter(w => w.word.split(c).length - 1 >= 2);
const single = group.filter(w => w.word.split(c).length - 1 === 1);

filteredWords.push(...multi);

if (multi.length < 10) {
const needed = 10 - multi.length;
single.sort((a, b) => {
const lenDiff = b.word.length - a.word.length;
if (lenDiff !== 0) return lenDiff;
return a.word.localeCompare(b.word);
});
filteredWords.push(...single.slice(0, needed));
}
});

return { data: { words: filteredWords, waitWords: [] }, error: null }
}
}
else if (224 <= name && name <= 237) {
const c = ['가', '나', '다', '라', '마', '바', '사', '아', '자', '차', '카', '타', '파', '하'][name - 224];
const bit = misssionCharMask([c]);
if (bit !== 0) {
const { data: missionKWordsData, error: missionKWordsError } = await this.supabase.rpc('get_mission_words', { target_mask: bit })
if (missionKWordsError) return { data: null, error: missionKWordsError }

const grouped: Record<string, NonNullable<typeof missionKWordsData>> = {};
(missionKWordsData || []).forEach(item => {
const f = item.word[item.word.length - 1];
if (!grouped[f]) grouped[f] = [];
grouped[f].push(item);
});

const filteredWords: NonNullable<typeof missionKWordsData> = [];
Object.values(grouped).forEach(group => {
const multi = group.filter(w => w.word.split(c).length - 1 >= 2);
const single = group.filter(w => w.word.split(c).length - 1 === 1);

filteredWords.push(...multi);

if (multi.length < 10) {
const needed = 10 - multi.length;
single.sort((a, b) => {
const lenDiff = b.word.length - a.word.length;
if (lenDiff !== 0) return lenDiff;
return a.word.localeCompare(b.word);
});
filteredWords.push(...single.slice(0, needed));
}
});

return { data: { words: filteredWords, waitWords: [] }, error: null }

}
} else if (239 <= name && name <= 252) {
const c = ['가', '나', '다', '라', '마', '바', '사', '아', '자', '차', '카', '타', '파', '하'][name - 239];
const bit = misssionCharMask([c]);
if (bit !== 0) {
const { data: missionKWordsData, error: missionKWordsError } = await this.supabase.rpc('get_mission_len3_words', { target_mask: bit })
if (missionKWordsError) return { data: null, error: missionKWordsError }

const grouped: Record<string, NonNullable<typeof missionKWordsData>> = {};
(missionKWordsData || []).forEach(item => {
const f = item.word[0];
if (!grouped[f]) grouped[f] = [];
grouped[f].push(item);
});

const filteredWords: NonNullable<typeof missionKWordsData> = [];
Object.values(grouped).forEach(group => {
const multi = group.filter(w => w.word.split(c).length - 1 >= 2);
const single = group.filter(w => w.word.split(c).length - 1 === 1);

filteredWords.push(...multi);

if (multi.length < 10) {
const needed = 10 - multi.length;
single.sort((a, b) => {
const lenDiff = b.word.length - a.word.length;
if (lenDiff !== 0) return lenDiff;
return a.word.localeCompare(b.word);
});
filteredWords.push(...single.slice(0, needed));
}
});

return { data: { words: filteredWords, waitWords: [] }, error: null }
}
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

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

The special ID ranges and logic are hardcoded in multiple places across the codebase. Consider extracting these magic numbers into named constants at the file or module level. For example: MISSION_WORD_FIRST_START = 209, MISSION_WORD_FIRST_END = 222, MISSION_WORD_LAST_START = 224, etc. This would improve code clarity and make it easier to maintain if these ranges need to change in the future.

Copilot uses AI. Check for mistakes.
return { data: { words: wordsData.filter(({ word }) => !waitWordsData.some(w => word === w.word)), waitWords: waitWordsData }, error: null }
} else if (209 <= name && name <= 222) {
const c = ['가', '나', '다', '라', '마', '바', '사', '아', '자', '차', '카', '타', '파', '하'][name - 209];
const bit = misssionCharMask([c]);
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

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

The function call contains a typo in the function name: "misssionCharMask" should be "missionCharMask" (three 's' instead of two).

Copilot uses AI. Check for mistakes.
}
else if (224 <= name && name <= 237) {
const c = ['가', '나', '다', '라', '마', '바', '사', '아', '자', '차', '카', '타', '파', '하'][name - 224];
const bit = misssionCharMask([c]);
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

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

The function call contains a typo in the function name: "misssionCharMask" should be "missionCharMask" (three 's' instead of two).

Copilot uses AI. Check for mistakes.
<Link href={`/word/search/${word}`} className="font-semibold text-gray-900 underline dark:text-gray-100">
{isMission.m && isMission.t ? (
word.split("").map((char, i) => (
char === isMission.t ? <span key={i} className="text-green-500">{char}</span> : char
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

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

The character highlighting logic uses array index as the key prop in the map function. When the same character appears multiple times in a word, this could lead to React reconciliation issues. Consider using a more unique key like char-${i} or combining the index with another unique identifier.

Suggested change
char === isMission.t ? <span key={i} className="text-green-500">{char}</span> : char
char === isMission.t ? <span key={`${word}-${i}`} className="text-green-500">{char}</span> : char

Copilot uses AI. Check for mistakes.
if (errorMessage) return <ErrorPage message={errorMessage}/>

if (wordsData) return <DocsDataHome id={id} data={wordsData.words.sort((a,b)=>a.word.localeCompare(b.word,'ko'))} metaData={wordsData.metadata} starCount={wordsData.starCount}/>
if (wordsData) return <DocsDataHome id={id} data={wordsData.words.sort((a,b)=>a.word.localeCompare(b.word,'ko'))} metaData={wordsData.metadata} starCount={wordsData.starCount} isSpecial={(209 <= id && id <= 222) || (224<= id && id <= 237) || (239<= id && id <= 252)}/>
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

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

The magic number calculations for determining if an ID is "special" are complex and duplicated. The expression (209 <= id && id <= 222) || (224<= id && id <= 237) || (239<= id && id <= 252) would be clearer if extracted into a helper function or checked against named constants. This also appears in other parts of the file and in SupabaseClientManager.ts, creating maintenance burden.

Copilot uses AI. Check for mistakes.
* @param chars 문자 배열
* @returns 미션 문자 마스크
*/
export function misssionCharMask(chars: string[]): number {
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

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

The function name contains a typo: "misssionCharMask" should be "missionCharMask" (three 's' instead of two). This typo is also present in the import statement in SupabaseClientManager.ts and will need to be corrected consistently across all files where this function is imported or called.

Copilot uses AI. Check for mistakes.
@hafskjfha hafskjfha merged commit a05b28b into dev Dec 29, 2025
10 checks passed
@hafskjfha hafskjfha deleted the feature/mission_word-page branch December 29, 2025 01:17
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.

한 끝/앞/쿵 미션단어 페이지

2 participants