diff --git a/docs/frontend-improvement.md b/docs/frontend-improvement.md
index 364238a..11c07d7 100644
--- a/docs/frontend-improvement.md
+++ b/docs/frontend-improvement.md
@@ -48,7 +48,7 @@ components/
| Schedules.jsx | 1465 | 1159 | ✅ 분리 완료 |
| ScheduleForm.jsx | 1047 | 765 | ✅ 분리 완료 |
| ScheduleDict.jsx | 714 | 572 | ✅ 분리 완료 |
-| AlbumForm.jsx | 631 | 631 | 미분리 |
+| AlbumForm.jsx | 631 | 443 | ✅ 분리 완료 |
---
@@ -96,8 +96,10 @@ components/
│ │ ├── ScheduleItem.jsx # 일정 아이템 (리스트용)
│ │ ├── LocationSearchDialog.jsx # 장소 검색 모달
│ │ ├── MemberSelector.jsx # 멤버 선택 UI
-│ │ └── ImageUploader.jsx # 이미지 업로드
-│ └── album/ # PhotoUploader 등
+│ │ ├── ImageUploader.jsx # 이미지 업로드
+│ │ └── WordItem.jsx # 사전 단어 아이템
+│ └── album/ # ✅ 추가된 컴포넌트들:
+│ └── TrackItem.jsx # 트랙 입력 폼
```
### 2.3 중복 코드 제거
@@ -179,8 +181,10 @@ pages/pc/admin/schedules/
3. [x] ScheduleDict.jsx 분리 (714줄 → 572줄, 142줄 감소)
- `WordItem.jsx` 추출 (단어 테이블 행 컴포넌트)
- `POS_TAGS` 상수 함께 분리
-4. [ ] AlbumPhotos.jsx 분리 (1536줄, 구조 복잡)
-5. [ ] AlbumForm.jsx 분리 (631줄)
+4. [x] AlbumForm.jsx 분리 (631줄 → 443줄, 188줄 감소)
+ - `CustomSelect.jsx` 추출 (공통 드롭다운 → common/)
+ - `TrackItem.jsx` 추출 (트랙 입력 폼 → album/)
+5. [ ] AlbumPhotos.jsx 분리 (1536줄, 구조 복잡)
### Phase 3: 추가 개선
1. [x] 관리자 페이지용 에러 페이지 추가 (404)
diff --git a/frontend-temp/src/components/pc/admin/album/TrackItem.jsx b/frontend-temp/src/components/pc/admin/album/TrackItem.jsx
new file mode 100644
index 0000000..7e4c2a1
--- /dev/null
+++ b/frontend-temp/src/components/pc/admin/album/TrackItem.jsx
@@ -0,0 +1,153 @@
+/**
+ * 앨범 트랙 입력 컴포넌트
+ */
+import { memo } from 'react';
+import { motion, AnimatePresence } from 'framer-motion';
+import { Trash2, Star, ChevronDown } from 'lucide-react';
+
+/**
+ * @param {Object} props
+ * @param {Object} props.track - 트랙 데이터
+ * @param {number} props.index - 트랙 인덱스
+ * @param {Function} props.onUpdate - 트랙 업데이트 핸들러 (index, field, value)
+ * @param {Function} props.onRemove - 트랙 삭제 핸들러 ()
+ */
+const TrackItem = memo(function TrackItem({ track, index, onUpdate, onRemove }) {
+ return (
+
+
+
+
+ {String(track.track_number).padStart(2, '0')}
+
+
+
+
+
+
+
+
+ {/* 상세 정보 토글 */}
+
+
+
+
+
+ {track.showDetails && (
+
+ {/* 작사/작곡/편곡 */}
+
+
+ {/* MV URL */}
+
+
+ onUpdate(index, 'music_video_url', e.target.value)}
+ className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
+ placeholder="https://youtube.com/watch?v=..."
+ />
+
+
+ {/* 가사 */}
+
+
+
+
+ )}
+
+
+ );
+});
+
+export default TrackItem;
diff --git a/frontend-temp/src/components/pc/admin/album/index.js b/frontend-temp/src/components/pc/admin/album/index.js
new file mode 100644
index 0000000..2d27312
--- /dev/null
+++ b/frontend-temp/src/components/pc/admin/album/index.js
@@ -0,0 +1 @@
+export { default as TrackItem } from './TrackItem';
diff --git a/frontend-temp/src/components/pc/admin/common/CustomSelect.jsx b/frontend-temp/src/components/pc/admin/common/CustomSelect.jsx
new file mode 100644
index 0000000..a32001c
--- /dev/null
+++ b/frontend-temp/src/components/pc/admin/common/CustomSelect.jsx
@@ -0,0 +1,74 @@
+/**
+ * 커스텀 드롭다운 셀렉트 컴포넌트
+ */
+import { useState, useRef, useEffect } from 'react';
+import { motion, AnimatePresence } from 'framer-motion';
+import { ChevronDown } from 'lucide-react';
+
+/**
+ * @param {Object} props
+ * @param {string} props.value - 선택된 값
+ * @param {Function} props.onChange - 값 변경 핸들러
+ * @param {string[]} props.options - 옵션 목록
+ * @param {string} props.placeholder - 플레이스홀더
+ */
+function CustomSelect({ value, onChange, options, placeholder }) {
+ const [isOpen, setIsOpen] = useState(false);
+ const ref = useRef(null);
+
+ useEffect(() => {
+ const handleClickOutside = (e) => {
+ if (ref.current && !ref.current.contains(e.target)) {
+ setIsOpen(false);
+ }
+ };
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => document.removeEventListener('mousedown', handleClickOutside);
+ }, []);
+
+ return (
+
+
+
+
+ {isOpen && (
+
+ {options.map((option) => (
+
+ ))}
+
+ )}
+
+
+ );
+}
+
+export default CustomSelect;
diff --git a/frontend-temp/src/components/pc/admin/common/index.js b/frontend-temp/src/components/pc/admin/common/index.js
index 7a69392..abe3a80 100644
--- a/frontend-temp/src/components/pc/admin/common/index.js
+++ b/frontend-temp/src/components/pc/admin/common/index.js
@@ -1,4 +1,5 @@
export { default as ConfirmDialog } from './ConfirmDialog';
+export { default as CustomSelect } from './CustomSelect';
export { default as DatePicker } from './DatePicker';
export { default as TimePicker } from './TimePicker';
export { default as NumberPicker } from './NumberPicker';
diff --git a/frontend-temp/src/components/pc/admin/index.js b/frontend-temp/src/components/pc/admin/index.js
index 8c47f41..2453081 100644
--- a/frontend-temp/src/components/pc/admin/index.js
+++ b/frontend-temp/src/components/pc/admin/index.js
@@ -6,3 +6,6 @@ export * from './common';
// 스케줄 관련
export * from './schedule';
+
+// 앨범 관련
+export * from './album';
diff --git a/frontend-temp/src/pages/pc/admin/albums/AlbumForm.jsx b/frontend-temp/src/pages/pc/admin/albums/AlbumForm.jsx
index f344f45..322351a 100644
--- a/frontend-temp/src/pages/pc/admin/albums/AlbumForm.jsx
+++ b/frontend-temp/src/pages/pc/admin/albums/AlbumForm.jsx
@@ -4,75 +4,15 @@
import { useState, useRef, useEffect } from 'react';
import { useNavigate, useParams, Link } from 'react-router-dom';
import { useQuery, useQueryClient } from '@tanstack/react-query';
-import { motion, AnimatePresence } from 'framer-motion';
-import { Save, Home, ChevronRight, Music, Trash2, Plus, Image, Star, ChevronDown } from 'lucide-react';
+import { motion } from 'framer-motion';
+import { Save, Home, ChevronRight, Music, Plus, Image } from 'lucide-react';
import { Toast } from '@/components/common';
-import { AdminLayout, DatePicker } from '@/components/pc/admin';
+import { AdminLayout, DatePicker, CustomSelect, TrackItem } from '@/components/pc/admin';
import { useAdminAuth } from '@/hooks/pc/admin';
import { useToast } from '@/hooks/common';
import { adminAlbumApi } from '@/api/admin';
import { fetchFormData } from '@/api/client';
-// 커스텀 드롭다운 컴포넌트
-function CustomSelect({ value, onChange, options, placeholder }) {
- const [isOpen, setIsOpen] = useState(false);
- const ref = useRef(null);
-
- useEffect(() => {
- const handleClickOutside = (e) => {
- if (ref.current && !ref.current.contains(e.target)) {
- setIsOpen(false);
- }
- };
- document.addEventListener('mousedown', handleClickOutside);
- return () => document.removeEventListener('mousedown', handleClickOutside);
- }, []);
-
- return (
-
-
-
-
- {isOpen && (
-
- {options.map((option) => (
-
- ))}
-
- )}
-
-
- );
-}
-
function AdminAlbumForm() {
const navigate = useNavigate();
const queryClient = useQueryClient();
@@ -454,141 +394,13 @@ function AdminAlbumForm() {
) : (
{tracks.map((track, index) => (
-
-
-
-
- {String(track.track_number).padStart(2, '0')}
-
-
-
-
-
-
-
-
- {/* 상세 정보 토글 */}
-
-
-
-
-
- {track.showDetails && (
-
- {/* 작사/작곡/편곡 */}
-
-
- {/* MV URL */}
-
-
- updateTrack(index, 'music_video_url', e.target.value)}
- className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
- placeholder="https://youtube.com/watch?v=..."
- />
-
-
- {/* 가사 */}
-
-
-
-
- )}
-
-
+
removeTrack(index)}
+ />
))}
)}