From 5521c44fa9cfa37e2426c46a660b9a60877423a4 Mon Sep 17 00:00:00 2001
From: caadiq
Date: Sun, 18 Jan 2026 13:53:51 +0900
Subject: [PATCH] =?UTF-8?q?feat:=20=EC=96=B4=EB=93=9C=EB=AF=BC=20=EC=82=AC?=
=?UTF-8?q?=EC=A0=84=20=EA=B4=80=EB=A6=AC=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?=
=?UTF-8?q?=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 형태소 분석기 사용자 사전 관리 페이지 추가
- 단어 추가/삭제/수정 시 즉시 저장 및 형태소 분석기 리로드
- 품사별 통계 및 필터링 기능
- 검색 기능 추가
백엔드:
- GET/PUT /api/schedules/suggestions/dict API 추가
- morpheme.js에 reloadMorpheme(), getUserDictPath() 함수 추가
프론트엔드:
- AdminScheduleDict.jsx 페이지 추가
- AdminSchedule.jsx에 사전 관리 버튼 추가
- 라우트 추가 (/admin/schedule/dict)
Co-Authored-By: Claude
---
backend/models/kiwi/user.dict | 59 +-
backend/src/routes/schedules/suggestions.js | 81 +++
backend/src/services/suggestions/morpheme.js | 19 +
frontend/src/App.jsx | 2 +
frontend/src/api/admin/suggestions.js | 17 +
frontend/src/pages/pc/admin/AdminSchedule.jsx | 11 +-
.../src/pages/pc/admin/AdminScheduleDict.jsx | 625 ++++++++++++++++++
7 files changed, 754 insertions(+), 60 deletions(-)
create mode 100644 frontend/src/api/admin/suggestions.js
create mode 100644 frontend/src/pages/pc/admin/AdminScheduleDict.jsx
diff --git a/backend/models/kiwi/user.dict b/backend/models/kiwi/user.dict
index d1c6887..7487cd7 100644
--- a/backend/models/kiwi/user.dict
+++ b/backend/models/kiwi/user.dict
@@ -1,15 +1,8 @@
-# 사용자 정의 사전
-# 형식: 단어\t품사\t점수(선택)
-# 품사: NNP(고유명사), NNG(일반명사), SL(외국어)
-
-# 그룹명
프로미스나인 NNP
프미나 NNP
프나 NNP
fromis_9 SL
fromis SL
-
-# 멤버 이름
이새롬 NNP
새롬 NNP
송하영 NNP
@@ -28,70 +21,20 @@ fromis SL
나경 NNP
백지헌 NNP
지헌 NNP
-
-# 팬덤
플로버 NNP
flover SL
-
-# 음악 프로그램
뮤직뱅크 NNP
인기가요 NNP
음악중심 NNP
엠카운트다운 NNP
쇼챔피언 NNP
더쇼 NNP
-
-# 앨범/곡명
-하얀그리움 NNP
-LIKE_YOU_BETTER SL
-
-# 콘텐츠/채널명
스프 NNP
성수기 NNP
이단장 NNP
슈퍼E나경 NNP
FM_1.24 SL
-벌거벗은한국사 NNP
-꿈친구 NNP
-미미미누 NNP
-비밀전학생 NNP
-하일병 NNP
-동네스타K쇼 NNP
-밥사효 NNP
-워크맨 NNP
-영업중 NNP
-개그콘서트 NNP
-
-# 방송/행사
-워터밤 NNP
-서든어택 NNP
-NPOP SL
-하이록스 NNP
-
-# 관련 용어
팬미팅 NNG
직캠 NNG
-시구 NNG
-시타 NNG
컴백 NNG
-호캉스 NNG
-팬캠 NNG
-응원법 NNG
-브이라이브 NNG
-브이앱 NNG
-
-# 다른 아이돌/연예인
-류진 NNP
-RYUJIN SL
-ITZY SL
-이즈나 NNP
-izna SL
-아일릿 NNP
-ILLIT SL
-키스오브라이프 NNP
-하이키 NNP
-H1KEY SL
-아이들 NNP
-미연 NNP
-효연 NNP
-T1 SL
+응원법 NNG
\ No newline at end of file
diff --git a/backend/src/routes/schedules/suggestions.js b/backend/src/routes/schedules/suggestions.js
index 3fa5859..7305bde 100644
--- a/backend/src/routes/schedules/suggestions.js
+++ b/backend/src/routes/schedules/suggestions.js
@@ -1,7 +1,9 @@
/**
* 추천 검색어 API 라우트
*/
+import { readFileSync, writeFileSync } from 'fs';
import { SuggestionService } from '../../services/suggestions/index.js';
+import { reloadMorpheme, getUserDictPath } from '../../services/suggestions/morpheme.js';
let suggestionService = null;
@@ -113,4 +115,83 @@ export default async function suggestionsRoutes(fastify) {
await suggestionService.saveSearchQuery(query);
return { success: true };
});
+
+ /**
+ * GET /api/schedules/suggestions/dict
+ * 사용자 사전 조회 (관리자 전용)
+ */
+ fastify.get('/dict', {
+ schema: {
+ tags: ['suggestions'],
+ summary: '사용자 사전 조회',
+ security: [{ bearerAuth: [] }],
+ response: {
+ 200: {
+ type: 'object',
+ properties: {
+ content: { type: 'string', description: '사전 내용' },
+ },
+ },
+ },
+ },
+ preHandler: [fastify.authenticate],
+ }, async (request, reply) => {
+ try {
+ const dictPath = getUserDictPath();
+ const content = readFileSync(dictPath, 'utf-8');
+ return { content };
+ } catch (error) {
+ if (error.code === 'ENOENT') {
+ return { content: '' };
+ }
+ throw error;
+ }
+ });
+
+ /**
+ * PUT /api/schedules/suggestions/dict
+ * 사용자 사전 저장 및 리로드 (관리자 전용)
+ */
+ fastify.put('/dict', {
+ schema: {
+ tags: ['suggestions'],
+ summary: '사용자 사전 저장',
+ security: [{ bearerAuth: [] }],
+ body: {
+ type: 'object',
+ required: ['content'],
+ properties: {
+ content: { type: 'string', description: '사전 내용' },
+ },
+ },
+ response: {
+ 200: {
+ type: 'object',
+ properties: {
+ success: { type: 'boolean' },
+ message: { type: 'string' },
+ },
+ },
+ },
+ },
+ preHandler: [fastify.authenticate],
+ }, async (request, reply) => {
+ const { content } = request.body;
+
+ try {
+ const dictPath = getUserDictPath();
+ writeFileSync(dictPath, content, 'utf-8');
+
+ // 형태소 분석기 리로드
+ await reloadMorpheme();
+
+ return { success: true, message: '사전이 저장되었습니다.' };
+ } catch (error) {
+ console.error('[Suggestions] 사전 저장 오류:', error.message);
+ return reply.code(500).send({
+ success: false,
+ message: '사전 저장 중 오류가 발생했습니다.',
+ });
+ }
+ });
}
diff --git a/backend/src/services/suggestions/morpheme.js b/backend/src/services/suggestions/morpheme.js
index d3a1b18..05d092c 100644
--- a/backend/src/services/suggestions/morpheme.js
+++ b/backend/src/services/suggestions/morpheme.js
@@ -162,3 +162,22 @@ function fallbackExtract(text) {
export function isReady() {
return isInitialized && kiwi !== null;
}
+
+/**
+ * 형태소 분석기 리로드 (사전 변경 시 호출)
+ */
+export async function reloadMorpheme() {
+ console.log('[Morpheme] 리로드 시작...');
+ isInitialized = false;
+ kiwi = null;
+ initPromise = null;
+ await initMorpheme();
+ console.log('[Morpheme] 리로드 완료');
+}
+
+/**
+ * 사용자 사전 파일 경로 반환
+ */
+export function getUserDictPath() {
+ return join(__dirname, '../../../models/kiwi', USER_DICT);
+}
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index e9a177a..75bfd13 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -40,6 +40,7 @@ import AdminSchedule from './pages/pc/admin/AdminSchedule';
import AdminScheduleForm from './pages/pc/admin/AdminScheduleForm';
import AdminScheduleCategory from './pages/pc/admin/AdminScheduleCategory';
import AdminScheduleBots from './pages/pc/admin/AdminScheduleBots';
+import AdminScheduleDict from './pages/pc/admin/AdminScheduleDict';
// 레이아웃
import PCLayout from './components/pc/Layout';
@@ -75,6 +76,7 @@ function App() {
} />
} />
} />
+ } />
{/* 일반 페이지 (레이아웃 포함) */}
fromis_9의 일정을 관리합니다
+