Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
cb733da
Merge pull request #97 from hafskjfha/dev
hafskjfha Aug 28, 2025
3ec345a
Merge pull request #98 from hafskjfha/dev
hafskjfha Sep 9, 2025
724177c
Merge pull request #99 from hafskjfha/dev
hafskjfha Sep 10, 2025
7dce708
Merge pull request #100 from hafskjfha/dev
hafskjfha Sep 11, 2025
d1a28aa
Merge pull request #101 from hafskjfha/dev
hafskjfha Sep 13, 2025
0a3af27
Merge pull request #102 from hafskjfha/dev
hafskjfha Sep 16, 2025
7c10510
v1.3.0
hafskjfha Nov 27, 2025
97b63cf
Merge pull request #105 from SolidLoop-studio/dev
hafskjfha Dec 12, 2025
84878bb
Bump version from 0.1.0 to 1.3.0
hafskjfha Dec 20, 2025
4eb0bec
[skip ci] bump version
hafskjfha Dec 20, 2025
0e16dff
Merge pull request #108 from SolidLoop-studio/dev
hafskjfha Dec 20, 2025
4d119bb
Fix React Server Components CVE vulnerabilities
vercel[bot] Dec 20, 2025
b795278
Merge pull request #109 from SolidLoop-studio/vercel/react-server-com…
hafskjfha Dec 20, 2025
3df63b6
Merge pull request #110 from SolidLoop-studio/dev
hafskjfha Dec 20, 2025
8a274de
[skip ci] - (v1.4.0)
github-actions[bot] Dec 20, 2025
bb337fd
[skip ci] - v1.4.0
hafskjfha Dec 20, 2025
59f5d7f
feat: Open API추가 및 api docs 추가
as7ar Dec 25, 2025
30e83f3
feat: renamed following Naming Conventions
as7ar Dec 25, 2025
b791e38
feat: split API page
as7ar Dec 27, 2025
cfe0eff
fix: 오탈자 제거
as7ar Dec 27, 2025
d69905f
Merge branch 'dev' into ASTAR
hafskjfha Dec 27, 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
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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# [v1.4.0](https://github.com/SolidLoop-studio/kkuko-utils/compare/v1.3.0...v1.4.0) - 2025-12-20

## fix
- ([b91fdd2](https://github.com/SolidLoop-studio/kkuko-utils/commit/b91fdd2)) - 릴리즈 nodejs 버전 업데이트
- ([e603226](https://github.com/SolidLoop-studio/kkuko-utils/commit/e603226)) - 서비스 제공자 이름 변경, 코드 라이센스 변경

## feat
- ([768ef96](https://github.com/SolidLoop-studio/kkuko-utils/commit/768ef96)) - implement automated release workflow and update dependencies

10 changes: 5 additions & 5 deletions app/AutoLogin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ const AutoLogin = () => {

if (!data || !data.session || error) return;

const { data: ddata, error: err } = await SCM.get().userById(data.session.user.id);
const { data: dbdata, error: err } = await SCM.get().userById(data.session.user.id);

if (err || !ddata) return;
if (err || !dbdata) return;

dispatch(
userAction.setInfo({
username: ddata.nickname,
role: ddata.role ?? "guest",
uuid: ddata.id,
username: dbdata.nickname,
role: dbdata.role ?? "guest",
uuid: dbdata.id,
})
);
}
Expand Down
4 changes: 2 additions & 2 deletions app/ErrorPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { ErrorMessage } from "./types/type";
import { useRouter } from "next/navigation";

const ErrorPage:React.FC<{e:ErrorMessage}> = ({e}) => {
const [errork,setError] = useState<ErrorMessage | null>(null);
const [error,setError] = useState<ErrorMessage | null>(null);
const router = useRouter();

const goBack = () => {
Expand All @@ -19,7 +19,7 @@ const ErrorPage:React.FC<{e:ErrorMessage}> = ({e}) => {

return (
<div className="flex flex-col flex-grow min-h-screen bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100">
{errork && <ErrorModal error={e} onClose={()=>setError(null)} /> }
{error && <ErrorModal error={e} onClose={()=>setError(null)} /> }

<button
onClick={goBack}
Expand Down
8 changes: 4 additions & 4 deletions app/admin/add-words/AddWordsHome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,7 @@ export default function WordsAddHome() {
const insertThemeMap: Record<string, string[]> = {}; // 단어 - 추가된 주제이름들 맵

// 단어 주제 추가
const { data: insertedThemesData, error: inseredThemeError } = await supabaseInQueryChunk(
const { data: insertedThemesData, error: insertedThemeError } = await supabaseInQueryChunk(
themesAddQuery,
async (chunk) => {
const r = await SCM.add().wordsThemes(chunk);
Expand All @@ -412,7 +412,7 @@ export default function WordsAddHome() {
}
}
)
if (inseredThemeError) return makeError(inseredThemeError)
if (insertedThemeError) return makeError(insertedThemeError)
for (const data of insertedThemesData ?? []) {
insertThemeMap[data.words.word] = [...(insertThemeMap[data.words.word] ?? []), data.themes.name]
}
Expand All @@ -438,9 +438,9 @@ export default function WordsAddHome() {
// JSON에 없는 단어-주제 쌍 제거
setCurrentTask('파일에 없는 단어-주제 쌍 제거 중...');
if (wordThemeDelQuery.length > 0) {
const { data: wordthemeDeletedData, error: wordThemeDeleteError } = await SCM.delete().wordTheme(wordThemeDelQuery);
const { data: wordThemeDeletedData, error: wordThemeDeleteError } = await SCM.delete().wordTheme(wordThemeDelQuery);
if (wordThemeDeleteError) return makeError(wordThemeDeleteError);
for (const data of wordthemeDeletedData) {
for (const data of wordThemeDeletedData) {
const docsId = themeDocsInfo[data.theme_name]
if (docsId) {
docsLogsQuery.push({
Expand Down
6 changes: 3 additions & 3 deletions app/admin/del-words/DelWordsHome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ export default function WordsDelHome() {

setCurrentTask("필요한 정보 가져오는 중...");
setProgress(0);
const { data: docsDatas, error: docsDataError } = await SCM.get().allDocs();
const { data: docsData, error: docsDataError } = await SCM.get().allDocs();
if (docsDataError) return makeError(docsDataError);

const { data: waitWords, error: waitWordsError } = await SCM.get().allWaitWords('delete');
Expand All @@ -156,10 +156,10 @@ export default function WordsDelHome() {
const themeDocsInfo: Record<string, number> = {};


docsDatas.filter(({ typez }) => typez === "letter").forEach(({ id, name }) => {
docsData.filter(({ typez }) => typez === "letter").forEach(({ id, name }) => {
letterDocsInfo[name] = id
})
docsDatas.filter(({ typez }) => typez === "theme").forEach(({ id, name }) => {
docsData.filter(({ typez }) => typez === "theme").forEach(({ id, name }) => {
themeDocsInfo[name] = id
})

Expand Down
2 changes: 1 addition & 1 deletion app/admin/logs/AdminLogsHome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ export default function AdminLogsHome({ initialWordLogs, initialDocsLogs, allDoc
setLoading(true);
try {
if (selectedTab === "word_logs") {
const { data, error } = await SCM.get().logsByFillter({
const { data, error } = await SCM.get().logsByFilter({
filterState: wordLogState,
filterType: wordLogType,
from: 0,
Expand Down
2 changes: 1 addition & 1 deletion app/admin/logs/AdminLogsWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export default function AdminLogsWrapper(){
{ data: allDocsLogs, error: docsLogsError },
{ data: allDocs, error: allDocsError }
] = await Promise.all([
SCM.get().logsByFillter({ filterState: "all", filterType: "all", from: 0, to: 999 }),
SCM.get().logsByFilter({ filterState: "all", filterType: "all", from: 0, to: 999 }),
SCM.get().docsLogsByFilter({ logType: "all", from: 0, to: 999 }),
SCM.get().allDocs()
]);
Expand Down
8 changes: 4 additions & 4 deletions app/admin/request-words/AdminRequestHome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ type WordRequest = {
word_id?: number; // 주제 변경 요청에서만 사용
}

export default function AdminHome({ requestDatas, refreshFn }: { requestDatas: WordRequest[], refreshFn: () => Promise<void> }) {
export default function AdminHome({ requestData: requestData, refreshFn }: { requestData: WordRequest[], refreshFn: () => Promise<void> }) {
const [selectedTab, setSelectedTab] = useState<string>("all");
const [selectedRequests, setSelectedRequests] = useState<Set<number>>(new Set());
const [selectedThemes, setSelectedThemes] = useState<Record<number, Set<number>>>({});
Expand All @@ -85,7 +85,7 @@ export default function AdminHome({ requestDatas, refreshFn }: { requestDatas: W
}, []);

// 요청 타입별 필터링
const filteredRequests = requestDatas.filter(request => {
const filteredRequests = requestData.filter(request => {
if (selectedTab === "all") return true;
return request.request_type === selectedTab;
});
Expand Down Expand Up @@ -169,7 +169,7 @@ export default function AdminHome({ requestDatas, refreshFn }: { requestDatas: W

// 승인 처리할 요청과 선택된 주제 정보 구성
const requestsToApprove = Array.from(selectedRequests).map(reqId => {
const request = requestDatas.find(r => r.id === reqId);
const request = requestData.find(r => r.id === reqId);
const selectedThemeIds = selectedThemes[reqId] || new Set<number>();

// allThemes에서 선택된 주제 정보 가져오기
Expand Down Expand Up @@ -427,7 +427,7 @@ export default function AdminHome({ requestDatas, refreshFn }: { requestDatas: W

// 거절할 처리할 요청과 선택된 주제 정보 구성
const requestsToReject = Array.from(selectedRequests).map(reqId => {
const request = requestDatas.find(r => r.id === reqId);
const request = requestData.find(r => r.id === reqId);
const selectedThemeIds = selectedThemes[reqId] || new Set<number>();

return {
Expand Down
8 changes: 4 additions & 4 deletions app/admin/request-words/AdminWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ type WordRequest = {
export default function AdminHomeWrapper(){
const { loadingState, updateLoadingState } = useLoadingState();
const [errorMessage,setErrorMessage] = useState<string|null>(null);
const [waitDatas,setWaitDatas] = useState<WordRequest[] | null>(null);
const [waitData,setWaitData] = useState<WordRequest[] | null>(null);

const MakeError = (error: PostgrestError) => {
setErrorMessage(`문서 정보 데이터 로드중 오류.\nErrorName: ${error.name ?? "알수없음"}\nError Message: ${error.message ?? "없음"}\nError code: ${error.code}`)
Expand Down Expand Up @@ -123,7 +123,7 @@ export default function AdminHomeWrapper(){
waitQueue.push(r);
});

setWaitDatas(waitQueue);
setWaitData(waitQueue);
updateLoadingState(100, "완료!")
}

Expand All @@ -141,7 +141,7 @@ export default function AdminHomeWrapper(){
return <ErrorPage message={errorMessage}/>
}

if (waitDatas){
return <AdminHome requestDatas={waitDatas} refreshFn={getWaitQueue} />
if (waitData){
return <AdminHome requestData={waitData} refreshFn={getWaitQueue} />
}
}
4 changes: 2 additions & 2 deletions app/api/auth/update_nickname/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ export async function POST(request: NextRequest){
if (!body){
return NextResponse.json({
data: null,
error: "invaild data"
error: "invalid data"
})
}

const {nickname} = body;
if (!nickname){
return NextResponse.json({
data: null,
error: "invaild data"
error: "invalid data"
})
}

Expand Down
59 changes: 59 additions & 0 deletions app/api/words/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@

---

## 엔드포인트
- **URL**: `/api/words/search`
- **Method**: `GET`
- **Description**: 모드별 필터링 및 정렬 옵션을 적용한 단어 리스트 반환

---

## 쿼리 파라미터

### 1. 필수 및 공통 옵션
| 파라미터 | 타입 | 설명 | 기본값 |
| :--- | :--- | :--- | :--- |
| `mode` | `string` | 게임 모드 (`kor-start`, `kor-end`, `kung`, `hunmin`, `jaqi`) | `kor-start` |
| `q` | `string` | **검색어.** 모드에 따라 시작자, 끝자, 또는 초성으로 동작 | - |
| `limit` | `number` | 최대 검색 결과 수 | `100` |
| `sortBy` | `string` | 정렬 기준 (`abc`: 가나다순, `length`: 글자수순, `attack`: 한방단어) | `length` |

### 2. 세부 필터링 (Advanced Options)
| 파라미터 | 타입 | 설명 | 기본값 |
| :--- | :--- | :--- | :--- |
| `manner` | `string` | 단어 필터 (`man`: 매너어, `jen`: 전어, `eti`: 에티켓) | `man` |
| `minLength` | `number` | 최소 글자 수 | `2` |
| `maxLength` | `number` | 최대 글자 수 | `100` |
| `duem` | `boolean` | 두음법칙 적용 여부 (`true`/`false`) | `true` |
| `mission` | `string` | 포함해야 할 특정 글자 (미션 파괴용) | `""` |
| `themeId` | `number` | `jaqi` 모드 사용 시 필수 테마 고유 ID | - |

---

## 예제

### A. 일반적인 끝말잇기 (시작 단어 찾기)
`가`로 시작하는 매너어 50개 검색 (글자수 순 정렬)
```http request
GET /api/words/search?mode=kor-start&q=가&manner=man&limit=50&sortBy=length
```
### B. 쿵쿵따 모드
`나`로 시작하는 3글자 단어 검색 (자동으로 3글자로 설정)
```http request
GET /api/words/search?mode=hunmin&q=ㄱㄴ
```
### C. 훈민정음 (초성 퀴즈)
`ㄱㄴ` 초성을 가진 단어 검색
```http request
GET /api/words/search?mode=hunmin&q=ㄱㄴ
```

## Response Status Code

* 200: OK
* 400: Bad Request
* 필수 파라미터(q) 누락
* 훈민정음 모드에서 2글자가 아닌 쿼리 전송
* 주제어 모드에서 themeId 누락
* 500: Internal Server Error
* 서버 내부 또는 데이터베이스 오류.
97 changes: 97 additions & 0 deletions app/api/words/search/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { NextRequest, NextResponse } from 'next/server';
import { SCM } from '@/app/lib/supabaseClient';
import { advancedQueryType } from '@/app/types/type';
import { GameMode } from '@/app/word/search/types';

export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);

const gameMode = (searchParams.get('mode') || 'kor-start') as GameMode;
const searchQuery = searchParams.get('q') || '';
const missionLetter = searchParams.get('mission') || '';
const minimumLength = parseInt(searchParams.get('minLength') || '2');
const maximumLength = parseInt(searchParams.get('maxLength') || '100');
const sortOrder = (searchParams.get('sortBy') || 'length') as 'abc' | 'length' | 'attack';
const isDuemApplied = searchParams.get('duem') !== 'false';
const hasMiniInfo = searchParams.get('miniInfo') === 'true';
const mannerMode = searchParams.get('manner') || 'man';
const isAcceptedOnly = searchParams.get('ingjung') !== 'false';
const displayLimit = parseInt(searchParams.get('limit') || '100');
const themeId = searchParams.get('themeId');

try {
let searchOptions: advancedQueryType;

if (gameMode === 'kor-start' || gameMode === 'kor-end') {
const startLetter = gameMode === 'kor-start' ? searchQuery : searchParams.get('start') || undefined;
const endLetter = gameMode === 'kor-end' ? searchQuery : searchParams.get('end') || undefined;

if (gameMode === 'kor-start' && !startLetter) return handleErrorResponse('시작 초성이 필요합니다.');
if (gameMode === 'kor-end' && !endLetter) return handleErrorResponse('끝 초성이 필요합니다.');

searchOptions = {
mode: gameMode,
start: startLetter?.trim(),
end: endLetter?.trim(),
mission: missionLetter,
ingjung: isAcceptedOnly,
man: mannerMode === 'man',
jen: mannerMode === 'jen',
eti: mannerMode === 'eti',
duem: isDuemApplied,
miniInfo: hasMiniInfo,
length_min: minimumLength,
length_max: maximumLength,
sort_by: sortOrder,
limit: isNaN(displayLimit) ? 100 : displayLimit
};
} else if (gameMode === 'kung') {
if (!searchQuery) return handleErrorResponse('단어가 필요합니다.');
searchOptions = {
mode: 'kung',
start: searchQuery.trim().slice(0, 3),
mission: missionLetter,
ingjung: isAcceptedOnly,
man: mannerMode === 'man',
jen: mannerMode === 'jen',
eti: mannerMode === 'eti',
duem: isDuemApplied,
miniInfo: hasMiniInfo,
length_min: 3,
length_max: 3,
sort_by: sortOrder,
limit: isNaN(displayLimit) ? 100 : displayLimit
};
} else if (gameMode === 'hunmin') {
if (searchQuery.trim().length !== 2) return handleErrorResponse('훈민정음 쿼리는 2글자여야 합니다.');
searchOptions = {
mode: 'hunmin',
query: searchQuery.trim(),
mission: missionLetter,
limit: isNaN(displayLimit) ? 100 : displayLimit
};
} else if (gameMode === 'jaqi') {
if (!themeId) return handleErrorResponse('주제 ID가 필요합니다.');
searchOptions = {
mode: 'jaqi',
query: searchQuery.trim(),
theme: Number(themeId),
limit: isNaN(displayLimit) ? 100 : displayLimit
};
} else {
return handleErrorResponse('유효하지 않은 모드입니다.');
}

const { data, error } = await SCM.get().wordsByAdvancedQuery(searchOptions);

if (error) throw error;

return NextResponse.json(data);
} catch (error) {
return NextResponse.json({ error: error }, { status: 500 });
}
}

function handleErrorResponse(message: string) {
return NextResponse.json({ error: message }, { status: 400 });
}
Loading