From 84cb3ad9e74458a349504317724a336c70902225 Mon Sep 17 00:00:00 2001 From: zenn Date: Fri, 24 Jul 2026 18:53:56 +0900 Subject: [PATCH] =?UTF-8?q?=EC=97=90=EB=94=94=ED=84=B0=20=EB=AF=B8?= =?UTF-8?q?=EB=94=94=EC=96=B4=20=EB=AA=A8=EB=8B=AC=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/admin/AdminMarkdownEditor.vue | 235 ++----------- .../admin/AdminMarkdownMediaPickerModal.vue | 315 ++++++++++++++++++ docs/map.md | 3 +- docs/update.md | 5 + lib/markdown-editor-media.js | 16 +- package-lock.json | 4 +- package.json | 2 +- tests/markdown-editor-media.test.js | 8 + 8 files changed, 378 insertions(+), 210 deletions(-) create mode 100644 components/admin/AdminMarkdownMediaPickerModal.vue diff --git a/components/admin/AdminMarkdownEditor.vue b/components/admin/AdminMarkdownEditor.vue index 9f7eb7d..093902c 100644 --- a/components/admin/AdminMarkdownEditor.vue +++ b/components/admin/AdminMarkdownEditor.vue @@ -33,12 +33,11 @@ import { getMediaPickerAcceptForTarget, getMediaPickerKindForTarget, getGallerySelectionOrder as getGallerySelectionOrderFromSelection, - getMediaItemKind, - getMediaPickerPreviewUrl, getMediaPickerTitleForTarget, isGalleryMediaPickerTarget, isMediaItemSelectableForTarget, isUploadFileAllowedForMediaTarget, + normalizeMediaPickerPage, normalizeMediaPickerResponse, resolveSelectedMediaItems, resolveUploadFetchErrorMessage, @@ -3076,7 +3075,7 @@ const getGallerySelectionOrder = (url) => { * @returns {Promise} */ const setMediaPickerPage = async (page) => { - mediaPickerPage.value = Math.min(Math.max(Number(page) || 1, 1), mediaPickerPageCount.value) + mediaPickerPage.value = normalizeMediaPickerPage(page, mediaPickerPageCount.value) await fetchMediaItems() } @@ -3627,206 +3626,36 @@ const handleKeydown = (event) => { -
-
-
-
-

- {{ mediaPickerTitle }} -

-
- -
- -
-
- - -
- -
- -
- -
-
-

- {{ isUploading ? '업로드 중입니다' : '파일을 끌어 업로드' }} -

-

- {{ isUploading ? '잠시만 기다려 주세요.' : '또는' }} -

- -

- {{ mediaPickerUploadHint }} -

-
-
-
-
-
-
-
- -
-
- {{ mediaPickerRangeLabel }} - -
-
- - -
-
-
-
+ diff --git a/components/admin/AdminMarkdownMediaPickerModal.vue b/components/admin/AdminMarkdownMediaPickerModal.vue new file mode 100644 index 0000000..358f9d3 --- /dev/null +++ b/components/admin/AdminMarkdownMediaPickerModal.vue @@ -0,0 +1,315 @@ + + + diff --git a/docs/map.md b/docs/map.md index 24ee161..2bb8f58 100644 --- a/docs/map.md +++ b/docs/map.md @@ -48,7 +48,7 @@ | lib/navigation-editor-tree.js | 관리자 메뉴 UI·서버 `renumberSortOrderByTree`가 쓰는 `buildNavigationEditorTree`, 관리자 상단 네비 평면 표용 `flattenNavigationEditorWrappers` | | lib/markdown-content-normalizer.js | 관리자 Markdown-first 전환 후 레거시 블록 배열·객체 본문 값을 저장용 마크다운 문자열로 변환 | | lib/markdown-editor-history.js | 관리자 Markdown-first 에디터 undo/redo 히스토리 항목 생성, 레거시 문자열 항목 본문 추출, 선택 범위 보정 | -| lib/markdown-editor-media.js | 관리자 Markdown-first 에디터 미디어 선택 모달의 미디어 종류 판별, 대상별 선택 가능 여부, 미리보기 URL, 업로드 MIME 허용, 선택 대상별 제목·accept·API kind·업로드 안내, 라이브러리 빈 상태 문구, 적용 버튼 상태, 선택 순서·토글·항목 해석, 업로드 오류·한도 검증, 페이지 상태·범위·페이지 번호 계산, fenced 미디어 블록 생성 | +| lib/markdown-editor-media.js | 관리자 Markdown-first 에디터 미디어 선택 모달의 미디어 종류 판별, 대상별 선택 가능 여부, 미리보기 URL, 업로드 MIME 허용, 선택 대상별 제목·accept·API kind·업로드 안내, 라이브러리 빈 상태 문구, 적용 버튼 상태, 선택 순서·토글·항목 해석, 업로드 오류·한도 검증, 페이지 상태·범위·페이지 번호 보정·페이지 번호 계산, fenced 미디어 블록 생성 | | lib/markdown-block-parser.js | 공개 Markdown 본문 문자열을 제목·문단·이미지·갤러리·fenced 블록·인용·목록·코드·표 렌더링 블록으로 변환 | | lib/markdown-block-context.js | 관리자 Markdown textarea·라이브 편집 포커스 위치 기준 이미지·갤러리·링크·iframe·HTML·인용·콜아웃·코드·토글 블록 설정 패널 대상 판별, 인용 옵션 공통 파서 연결, 코드 fenced 블록 여닫는 펜스 쌍 판별 | | lib/markdown-iframe-embed.js | 게시물 본문 iframe 블록의 코드 판별과 안전 속성 추출, `allowfullscreen`을 `allow=fullscreen` 권한으로 정규화 | @@ -218,6 +218,7 @@ | components/admin/AdminMediaDetailModal.vue | 관리자 미디어 화면의 상세 모달(미리보기·파일 정보·썸네일 상태·폴더·사용 현황·파일명·삭제 액션 표시) | | components/admin/AdminMediaVideoThumbnail.vue | 관리자 미디어 목록 비디오 항목의 초반 프레임 캔버스 썸네일 | | components/admin/AdminPostForm.vue | 관리자 글 작성/수정 폼, Ghost형 툴바(왼쪽 상태 텍스트·Publish/Update/Unpublish/Unschedule, 서버 반영 상태 기준 분기), 초안만 서버 디바운스 자동 저장·자동 저장 실패 payload 반복 재시도 차단·자동 저장 실패/진행 중 이탈 차단·신규 임시 슬러그·중복 슬러그 필드 하단 오류 표시·중복 슬러그 중 Publish/Update/최종 저장 비활성화·저장 중 추가 변경 재자동저장·발행 저장 중 이탈 확인·발행·예약·멤버십·비공개 상태 저장, 미저장 비교에서 태그 표시명 보조값 제외, 공개 상태 전환 시 빈 발행 시각 현재 시각 보정, 발행 모달(중앙 배치), 설정 패널 상태와 이벤트 연결, 미리보기 emit·미저장 이탈 가드, 자동 생성 썸네일을 숨기는 원본 전용 대표 이미지 선택(서버 페이지네이션·파일명/사용처 검색·번호형 페이지 이동·작은 파생 이미지 우선 미리보기·고정 높이 모달), 대표 이미지 본문 상단 표시 토글, 추천 글 토글, 사이트 설정 기준 태그 최대 개수 제한, 태그 색상 배지 다중 입력·최근 사용 태그 5개 우선 추천·메인 태그 드롭다운·부분 검색 추천, 새 태그 입력명 유지와 `/`→`-` 슬러그 변환 | +| components/admin/AdminMarkdownMediaPickerModal.vue | 관리자 Markdown 에디터 미디어 선택 모달 UI(업로드·라이브러리 탭, 검색 입력, 미디어 그리드, 업로드 영역, 페이지네이션, 적용/취소 버튼) | | components/admin/AdminPostSettingsSidebar.vue | 관리자 게시물 작성/수정 우측 설정 사이드바(Post URL·상태·발행 시각·요약·태그·대표 이미지 표시·추천 글·검색엔진 노출·본문 통계·삭제 버튼·블록 설정 패널 오버레이) | | components/admin/AdminPageForm.vue | 관리자 페이지 작성/수정 폼, 게시글 작성과 같은 전체 화면 에디터·상단 저장 툴바·접이식 오른쪽 설정 패널, 저장 payload 기준 미저장 비교·라우트/브라우저 이탈 확인, 페이지 공개 상태 선택, HTML 문서 기본 모드, 홈 Pages 진열장 노출 메타와 등록된 페이지 섹션 검색형 선택, 빈 본문/`!`+Tab HTML 골격 자동 완성, 항상 보이는 일반 텍스트/HTML 모드 선택, 한글 제목 영문 슬러그 자동 변환, HTML textarea 커서 위치 파일 URL 삽입, HTML 태그 내부 스마트 따옴표 정규화 | | components/admin/AdminMarkdownEditor.vue | 관리자 글 Markdown-first 에디터, 라이브·소스 모드 `/` 슬래시 명령(`/표` 기본 표 템플릿 포함)·미디어 모달(이미지·갤러리·비디오·오디오·파일, 업로드 탭 기본·첫 순서, 업로드 완료 파일 자동 선택, 라이브러리 탭은 `/admin/api/media?paged=1` 서버 페이지 응답·파일명/사용처 제목 검색·번호형 페이지 이동, 카드·본문·라이트박스·Pages 커버 자동 생성 이미지 숨김, 원본 URL 선택 유지·갤러리 선택 순서 배지·목록 미리보기는 720px 본문 파생 이미지 또는 카드 썸네일 우선, 검색 결과 수와 무관한 고정 높이 패널, 업로드 중 중복 업로드·닫기 차단·로딩 표시), 코드 블록 본문 슬래시 명령 비활성화, 커서 블록 컨텍스트·선택 텍스트 색상 컨텍스트 `block-panel` emit, 소스 모드 입력 직후 textarea 최신 본문 기준 커서 논리 줄·블록 컨텍스트 갱신, 에디터 본문 `Cmd/Ctrl+Z` 히스토리와 선택 위치 복원, 라이브 이미지 설정 패널·이미지↔갤러리 드래그 변환(`merge-images-to-gallery`·`insert-image-to-gallery`·`extract-gallery-image`), 소스·라이브 `Cmd+Shift+K` 줄 삭제, 소스·라이브 `Cmd/Ctrl+K` 링크 삽입, 코드·콜아웃·토글 내부 줄 삭제, 라이브 fenced 블록 현재 닫는 줄 기준 교체, 블록 패널 바깥 클릭·지원 블록 밖 커서 이동 시 닫기·미디어 모달 중 유지, 인용 마지막 줄 아래 방향키 외부 문단 포커스 이동, 인용·콜아웃·코드·토글 선언 줄 옵션 수정, IME 조합 중 현재 블록 범위 안에서만 블록 패널 유지, 소스 모드 wrap 라인 번호 보정·라이브↔소스 위치 복원(코드·콜아웃·토글 선언 줄은 본문 줄로 보정), 라이브 슬래시 명령 후 포커스 복원 지연·미디어 명령 원문 줄 재반영 방지 | diff --git a/docs/update.md b/docs/update.md index 23b20c3..52e265e 100644 --- a/docs/update.md +++ b/docs/update.md @@ -1,5 +1,10 @@ # 업데이트 이력 +## v1.11.78 + +- 관리자 Markdown 에디터의 미디어 선택 모달 UI를 `AdminMarkdownMediaPickerModal` 컴포넌트로 분리. +- 미디어 선택 페이지 번호 보정 helper 추가 및 단위 테스트 보강. + ## v1.11.77 - 관리자 Markdown 에디터 미디어 선택 적용 버튼 상태 계산을 공통 helper로 분리. diff --git a/lib/markdown-editor-media.js b/lib/markdown-editor-media.js index 8fd4b07..2b66eb7 100644 --- a/lib/markdown-editor-media.js +++ b/lib/markdown-editor-media.js @@ -434,6 +434,17 @@ export const findUploadSizeLimitError = ({ return null } +/** + * 미디어 선택 페이지 번호를 유효 범위로 보정한다. + * @param {number|string} page - 요청 페이지 + * @param {number|string} pageCount - 전체 페이지 수 + * @returns {number} 보정된 페이지 + */ +export const normalizeMediaPickerPage = (page, pageCount) => { + const total = Math.max(1, Number(pageCount) || 1) + return Math.min(Math.max(Number(page) || 1, 1), total) +} + /** * 미디어 선택 모달의 페이지 범위 표시 문구를 만든다. * @param {Object} options - 범위 표시 옵션 @@ -455,8 +466,7 @@ export const buildMediaPickerRangeLabel = ({ return '0개' } - const normalizedPageCount = Math.max(1, Number(pageCount) || 1) - const page = Math.min(Math.max(Number(pagination?.page) || currentPage, 1), normalizedPageCount) + const page = normalizeMediaPickerPage(Number(pagination?.page) || currentPage, pageCount) const pageSize = Number(pagination?.pageSize) || defaultPageSize const start = ((page - 1) * pageSize) + 1 const end = Math.min(start + pageSize - 1, total) @@ -476,7 +486,7 @@ export const buildMediaPickerPageItems = ({ pageCount }) => { const total = Math.max(1, Number(pageCount) || 1) - const current = Math.min(Math.max(Number(currentPage) || 1, 1), total) + const current = normalizeMediaPickerPage(currentPage, total) const pages = new Set([1, total, current]) for (let page = current - 2; page <= current + 2; page += 1) { diff --git a/package-lock.json b/package-lock.json index 5607d03..fca917f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sori.studio", - "version": "1.11.77", + "version": "1.11.78", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sori.studio", - "version": "1.11.77", + "version": "1.11.78", "hasInstallScript": true, "dependencies": { "@nuxtjs/tailwindcss": "^6.14.0", diff --git a/package.json b/package.json index 5d37d99..aa80365 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sori.studio", - "version": "1.11.77", + "version": "1.11.78", "private": true, "type": "module", "imports": { diff --git a/tests/markdown-editor-media.test.js b/tests/markdown-editor-media.test.js index 7113870..a2ba952 100644 --- a/tests/markdown-editor-media.test.js +++ b/tests/markdown-editor-media.test.js @@ -19,6 +19,7 @@ import { isGalleryMediaPickerTarget, isMediaItemSelectableForTarget, isUploadFileAllowedForMediaTarget, + normalizeMediaPickerPage, normalizeMediaPickerResponse, resolveSelectedMediaItems, resolveUploadFetchErrorMessage, @@ -273,6 +274,13 @@ test('업로드 파일 한도 초과 메시지를 계산한다', () => { }), null) }) +test('미디어 선택 페이지 번호를 유효 범위로 보정한다', () => { + assert.equal(normalizeMediaPickerPage(3, 5), 3) + assert.equal(normalizeMediaPickerPage('99', 5), 5) + assert.equal(normalizeMediaPickerPage(-1, 5), 1) + assert.equal(normalizeMediaPickerPage('bad', 0), 1) +}) + test('미디어 선택 페이지 범위 문구를 계산한다', () => { assert.equal(buildMediaPickerRangeLabel({ pagination: {