From e31cb8264913b2c1b8ae117f5e71f0eabe1c3eaf Mon Sep 17 00:00:00 2001 From: caadiq Date: Thu, 22 Jan 2026 23:31:58 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20ScheduleDict.jsx=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC=20-=20WordItem=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8?= =?UTF-8?q?=20=EC=B6=94=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - WordItem.jsx 컴포넌트 추출 (단어 테이블 행 + 품사 드롭다운) - POS_TAGS 상수 분리하여 export - ScheduleDict.jsx: 714줄 → 572줄 (142줄 감소) - 일정 관련 대형 파일 분리 완료 Co-Authored-By: Claude Opus 4.5 --- docs/frontend-improvement.md | 8 +- .../components/pc/admin/schedule/WordItem.jsx | 168 ++++++++++++++++++ .../src/components/pc/admin/schedule/index.js | 1 + .../pages/pc/admin/schedules/ScheduleDict.jsx | 146 +-------------- 4 files changed, 176 insertions(+), 147 deletions(-) create mode 100644 frontend-temp/src/components/pc/admin/schedule/WordItem.jsx diff --git a/docs/frontend-improvement.md b/docs/frontend-improvement.md index ecbcd49..364238a 100644 --- a/docs/frontend-improvement.md +++ b/docs/frontend-improvement.md @@ -47,7 +47,7 @@ components/ | AlbumPhotos.jsx | 1536 | 1536 | 미분리 | | Schedules.jsx | 1465 | 1159 | ✅ 분리 완료 | | ScheduleForm.jsx | 1047 | 765 | ✅ 분리 완료 | -| ScheduleDict.jsx | 714 | 714 | 미분리 | +| ScheduleDict.jsx | 714 | 572 | ✅ 분리 완료 | | AlbumForm.jsx | 631 | 631 | 미분리 | --- @@ -176,8 +176,10 @@ pages/pc/admin/schedules/ - `LocationSearchDialog.jsx` 추출 (장소 검색 모달) - `MemberSelector.jsx` 추출 (멤버 선택 UI) - `ImageUploader.jsx` 추출 (이미지 업로드 및 드래그앤드롭) -3. [ ] AlbumPhotos.jsx 분리 (1536줄, 구조 복잡) -4. [ ] ScheduleDict.jsx 분리 (714줄) +3. [x] ScheduleDict.jsx 분리 (714줄 → 572줄, 142줄 감소) + - `WordItem.jsx` 추출 (단어 테이블 행 컴포넌트) + - `POS_TAGS` 상수 함께 분리 +4. [ ] AlbumPhotos.jsx 분리 (1536줄, 구조 복잡) 5. [ ] AlbumForm.jsx 분리 (631줄) ### Phase 3: 추가 개선 diff --git a/frontend-temp/src/components/pc/admin/schedule/WordItem.jsx b/frontend-temp/src/components/pc/admin/schedule/WordItem.jsx new file mode 100644 index 0000000..8c7e4f2 --- /dev/null +++ b/frontend-temp/src/components/pc/admin/schedule/WordItem.jsx @@ -0,0 +1,168 @@ +/** + * 사전 단어 항목 컴포넌트 + * - 사전 관리 페이지의 단어 테이블 행 + */ +import { useState, useEffect, useRef } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { Trash2, ChevronDown } from 'lucide-react'; + +/** + * 품사 태그 옵션 + */ +export const POS_TAGS = [ + { + value: 'NNP', + label: '고유명사 (NNP)', + description: '사람, 그룹, 프로그램 이름 등', + examples: '프로미스나인, 송하영, 뮤직뱅크', + }, + { + value: 'NNG', + label: '일반명사 (NNG)', + description: '일반적인 명사', + examples: '직캠, 팬미팅, 콘서트', + }, + { + value: 'SL', + label: '외국어 (SL)', + description: '영어 등 외국어 단어', + examples: 'fromis_9, YouTube, fromm', + }, +]; + +/** + * 단어 항목 컴포넌트 + * @param {Object} props + * @param {string} props.id - 단어 고유 ID + * @param {string} props.word - 단어 + * @param {string} props.pos - 품사 태그 + * @param {number} props.index - 목록 인덱스 + * @param {Function} props.onUpdate - 수정 핸들러 (id, word, pos) + * @param {Function} props.onDelete - 삭제 핸들러 () + */ +function WordItem({ id, word, pos, index, onUpdate, onDelete }) { + const [isEditing, setIsEditing] = useState(false); + const [editWord, setEditWord] = useState(word); + const [editPos, setEditPos] = useState(pos); + const [showPosDropdown, setShowPosDropdown] = useState(false); + const dropdownRef = useRef(null); + + // 외부 클릭 시 드롭다운 닫기 + useEffect(() => { + const handleClickOutside = (event) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target)) { + setShowPosDropdown(false); + } + }; + + if (showPosDropdown) { + document.addEventListener('mousedown', handleClickOutside); + } + return () => document.removeEventListener('mousedown', handleClickOutside); + }, [showPosDropdown]); + + const handleSave = () => { + if (editWord.trim() && (editWord.trim() !== word || editPos !== pos)) { + onUpdate(id, editWord.trim(), editPos); + } + setIsEditing(false); + }; + + const handleKeyDown = (e) => { + if (e.key === 'Enter') { + handleSave(); + } else if (e.key === 'Escape') { + setEditWord(word); + setEditPos(pos); + setIsEditing(false); + } + }; + + return ( + + {index + 1} + + {isEditing ? ( + setEditWord(e.target.value)} + onKeyDown={handleKeyDown} + onBlur={handleSave} + autoFocus + className="w-full px-3 py-1.5 border border-primary rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/20" + /> + ) : ( + setIsEditing(true)} + className="cursor-pointer hover:text-primary transition-colors font-medium" + > + {word} + + )} + + +
+ + + {showPosDropdown && ( + + {POS_TAGS.map((tag) => ( + + ))} + + )} + +
+ + + + +
+ ); +} + +export default WordItem; diff --git a/frontend-temp/src/components/pc/admin/schedule/index.js b/frontend-temp/src/components/pc/admin/schedule/index.js index e7c9d91..3151963 100644 --- a/frontend-temp/src/components/pc/admin/schedule/index.js +++ b/frontend-temp/src/components/pc/admin/schedule/index.js @@ -4,3 +4,4 @@ export { default as ScheduleItem, getEditPath } from './ScheduleItem'; export { default as LocationSearchDialog } from './LocationSearchDialog'; export { default as MemberSelector } from './MemberSelector'; export { default as ImageUploader } from './ImageUploader'; +export { default as WordItem, POS_TAGS } from './WordItem'; diff --git a/frontend-temp/src/pages/pc/admin/schedules/ScheduleDict.jsx b/frontend-temp/src/pages/pc/admin/schedules/ScheduleDict.jsx index 09d0b5d..9c5cdbe 100644 --- a/frontend-temp/src/pages/pc/admin/schedules/ScheduleDict.jsx +++ b/frontend-temp/src/pages/pc/admin/schedules/ScheduleDict.jsx @@ -2,9 +2,10 @@ import { useState, useEffect, useMemo, useRef, useCallback } from 'react'; import { Link } from 'react-router-dom'; import { motion, AnimatePresence } from 'framer-motion'; import { useQuery } from '@tanstack/react-query'; -import { Home, ChevronRight, Book, Plus, Trash2, Search, ChevronDown } from 'lucide-react'; +import { Home, ChevronRight, Book, Plus, Search, ChevronDown } from 'lucide-react'; import { Toast } from '@/components/common'; import { AdminLayout, ConfirmDialog } from '@/components/pc/admin'; +import { WordItem, POS_TAGS } from '@/components/pc/admin/schedule'; import { useAdminAuth } from '@/hooks/pc/admin'; import { useToast } from '@/hooks/common'; import * as suggestionsApi from '@/api/admin/suggestions'; @@ -38,149 +39,6 @@ const cardVariants = { }, }; -// 품사 태그 옵션 -const POS_TAGS = [ - { - value: 'NNP', - label: '고유명사 (NNP)', - description: '사람, 그룹, 프로그램 이름 등', - examples: '프로미스나인, 송하영, 뮤직뱅크', - }, - { value: 'NNG', label: '일반명사 (NNG)', description: '일반적인 명사', examples: '직캠, 팬미팅, 콘서트' }, - { - value: 'SL', - label: '외국어 (SL)', - description: '영어 등 외국어 단어', - examples: 'fromis_9, YouTube, fromm', - }, -]; - -// 단어 항목 컴포넌트 -function WordItem({ id, word, pos, index, onUpdate, onDelete }) { - const [isEditing, setIsEditing] = useState(false); - const [editWord, setEditWord] = useState(word); - const [editPos, setEditPos] = useState(pos); - const [showPosDropdown, setShowPosDropdown] = useState(false); - const dropdownRef = useRef(null); - - // 외부 클릭 시 드롭다운 닫기 - useEffect(() => { - const handleClickOutside = (event) => { - if (dropdownRef.current && !dropdownRef.current.contains(event.target)) { - setShowPosDropdown(false); - } - }; - - if (showPosDropdown) { - document.addEventListener('mousedown', handleClickOutside); - } - return () => document.removeEventListener('mousedown', handleClickOutside); - }, [showPosDropdown]); - - const handleSave = () => { - if (editWord.trim() && (editWord.trim() !== word || editPos !== pos)) { - onUpdate(id, editWord.trim(), editPos); - } - setIsEditing(false); - }; - - const handleKeyDown = (e) => { - if (e.key === 'Enter') { - handleSave(); - } else if (e.key === 'Escape') { - setEditWord(word); - setEditPos(pos); - setIsEditing(false); - } - }; - - return ( - - {index + 1} - - {isEditing ? ( - setEditWord(e.target.value)} - onKeyDown={handleKeyDown} - onBlur={handleSave} - autoFocus - className="w-full px-3 py-1.5 border border-primary rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/20" - /> - ) : ( - setIsEditing(true)} - className="cursor-pointer hover:text-primary transition-colors font-medium" - > - {word} - - )} - - -
- - - {showPosDropdown && ( - - {POS_TAGS.map((tag) => ( - - ))} - - )} - -
- - - - -
- ); -} - function ScheduleDict() { const { user, isAuthenticated } = useAdminAuth(); const { toast, setToast } = useToast();