diff --git a/docs/changelog.md b/docs/changelog.md index b9d498f..e607f65 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # 업데이트 요약 +## v1.5.32 + +- 최근 내보내기 작업 카드에서 불필요한 요청일·분할 설정 정보를 줄이고 만료일 중심으로 정리했다. +- 완료된 백업은 진행도 박스를 숨기고, 파일 체크 선택 후 선택한 ZIP만 내려받을 수 있게 했다. + ## v1.5.31 - 게시물 내보내기 설정 카드와 다운로드 가능한 최근 작업 카드를 분리해 백업 요청과 결과 확인을 명확히 구분했다. diff --git a/docs/deploy.md b/docs/deploy.md index 9b7e5ca..3b3a79c 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -1,6 +1,6 @@ # 배포 가이드 -> 로컬 기준 v1.5.31에서 `npm run lint`, `npm run build` 검증을 통과했다. NAS 실제 컨테이너 기동과 도메인/프록시 접속 검증은 운영 배포 단계에서 진행한다. +> 로컬 기준 v1.5.32에서 `npm run lint`, `npm run build` 검증을 통과했다. NAS 실제 컨테이너 기동과 도메인/프록시 접속 검증은 운영 배포 단계에서 진행한다. ## 빌드 유형 diff --git a/docs/history.md b/docs/history.md index 12dde1d..3dba417 100644 --- a/docs/history.md +++ b/docs/history.md @@ -1,5 +1,9 @@ # 의사결정 이력 +## 2026-06-02 v1.5.32 — 완료된 백업은 선택 다운로드만 남긴다 + +내보내기 작업이 준비 완료된 뒤에는 진행도와 요청 조건보다 어떤 ZIP을 받을지가 더 중요하다. 한 파일만 있을 때도 일괄 다운로드와 개별 다운로드 버튼이 동시에 보이면 행동이 중복되어 보이므로, 완료 작업은 만료일만 남기고 파일 체크 선택과 선택 파일 다운로드로 정리한다. 진행도는 대기·생성 중 작업의 상태 확인용으로만 표시한다. + ## 2026-06-02 v1.5.31 — 백업 요청과 다운로드 결과는 분리해서 보여 준다 내보내기 설정 카드는 새 백업 작업을 만드는 입력 영역이고, 최근 작업 목록은 이미 생성되었거나 생성 중인 결과 영역이다. 두 흐름을 같은 카드 안에 두면 접기·펼치기 상태와 다운로드 상태가 한 덩어리처럼 보이므로, 최근 작업은 작업이 있을 때만 별도 카드로 표시해 요청 설정과 결과 확인을 분리한다. diff --git a/docs/map.md b/docs/map.md index 66d1c69..692d636 100644 --- a/docs/map.md +++ b/docs/map.md @@ -140,7 +140,7 @@ | pages/admin/tags/index.vue | 태그 관리(메인 태그 드래그 정렬 자동 저장·more vert 메뉴, 일반 태그 배지 more vert 메뉴·검색/정렬, 태그 추가 버튼), 액션 피드백 토스트 | | pages/admin/tags/new.vue | 태그 생성 | | pages/admin/tags/[id].vue | 태그 수정 | -| pages/admin/settings/index.vue | 사이트 설정 Ghost형 전체 화면, **POST 설정**(`showPostUpdatedAt` 토글·읽기 모드 비활성 톤), 블로그 제목·설명, **사이트 정보**(로고·URL·저작권), **메인 화면**(라이트·다크 커버 상하 개별 프리뷰·드롭존 업로드·오버레이 텍스트), **어나운스 바**(사용 토글·맞춤 설정·배경색·읽기 모드 비활성 톤), **스팸 필터**(가입 금지 닉네임), **게시물 내보내기** 독립 카드와 펼침형 전체·연도·월·직접 날짜 범위 작업 요청·목표 ZIP 용량·ZIP당 최대 게시물 수, 작업이 있을 때만 표시되는 **최근 내보내기 작업** 별도 카드(진행도·준비 완료 분할 파일 다운로드·브라우저 순차 일괄 다운로드·실패 작업 재시도·실패 상세 오류·작업 삭제), **게시물 가져오기** 독립 카드와 펼침형 ZIP 드롭존·적용 버튼·완료 요약·누락 자산 경고 표시, 진행 중 요청 버튼 잠금 | +| pages/admin/settings/index.vue | 사이트 설정 Ghost형 전체 화면, **POST 설정**(`showPostUpdatedAt` 토글·읽기 모드 비활성 톤), 블로그 제목·설명, **사이트 정보**(로고·URL·저작권), **메인 화면**(라이트·다크 커버 상하 개별 프리뷰·드롭존 업로드·오버레이 텍스트), **어나운스 바**(사용 토글·맞춤 설정·배경색·읽기 모드 비활성 톤), **스팸 필터**(가입 금지 닉네임), **게시물 내보내기** 독립 카드와 펼침형 전체·연도·월·직접 날짜 범위 작업 요청·목표 ZIP 용량·ZIP당 최대 게시물 수, 작업이 있을 때만 표시되는 **최근 내보내기 작업** 별도 카드(진행 중 진행도·만료일·파일 체크 선택·전체 선택·선택 파일 다운로드·실패 작업 재시도·실패 상세 오류·작업 삭제), **게시물 가져오기** 독립 카드와 펼침형 ZIP 드롭존·적용 버튼·완료 요약·누락 자산 경고 표시, 진행 중 요청 버튼 잠금 | | lib/signup-blocked-usernames.js | 가입 금지 닉네임 정리·매칭·안내 문구 | | pages/admin/members/index.vue | 관리자 멤버 목록(Ghost형 테이블, 글 목록과 같은 테두리형 검색, 조건 필터, 멤버 추가 버튼, 닉네임+이메일, 등급+비활성 상태, 가입일+최근 활동, IP, 댓글 수) | | pages/admin/members/new.vue | 관리자 멤버 추가(썸네일 URL, 이름, 이메일, 레이블, 관리자 노트) | diff --git a/docs/spec.md b/docs/spec.md index 308cdaa..1b2b7d9 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -697,9 +697,9 @@ components/content/ ### 사이트 설정 -- 관리자 사이트 설정 UI는 `/admin/settings`에서 제공한다. Ghost Admin과 유사하게 **전체 화면**으로 표시하며, 좌측 내비와 우측 본문을 **한 덩어리로 중앙 정렬**(`max-w` 래퍼, 본문 카드 영역은 약 760px 상한)하고, 페이지 배경은 밝은 회색·본문 열은 흰색으로 구분한다. 우측 본문을 스크롤하면 현재 보이는 구역에 맞춰 좌측 메뉴 활성 배경을 갱신한다. **우측 상단 고정** 닫기 버튼과 `Escape` 키로 설정 화면을 닫으며, 브라우저 히스토리가 있으면 `뒤로 가기`, 없으면 `/admin`으로 이동한다. 게시물 백업 도구는 `게시물 내보내기`와 `게시물 가져오기` 독립 카드로 표시한다. 내보내기 기본 화면은 제목·설명·내보내기 버튼만 표시하고, 상세 설정은 버튼을 눌렀을 때만 펼쳐진다. 내보내기 상세는 전체·특정년·특정월·직접 지정 범위, 목표 ZIP 용량, ZIP당 최대 게시물 수 지정을 제공한다. 내보내기 작업 목록은 작업이 있을 때만 내보내기 설정 카드 아래 별도 카드로 표시하며, 진행도, 준비 완료 분할 파일 다운로드, 브라우저 순차 일괄 다운로드, 실패 작업 재시도, 실패 상세 오류, 완료·실패 작업 삭제를 제공한다. 가져오기 기본 화면은 제목·설명·가져오기 버튼만 표시하고, 버튼을 누르면 ZIP 드롭존과 `적용` 버튼이 펼쳐진다. 가져오기는 파일 선택만으로 실행하지 않고 `적용` 버튼을 눌렀을 때만 Import API를 호출한다. 대기 중·생성 중 작업이 있으면 새 내보내기 요청 버튼을 비활성화한다. **스팸 필터**는 가입 금지 닉네임을 저장한다. **블로그 제목·설명** 카드는 기본적으로 사이트 이름·설명을 읽기 전용으로 보여 주고, `편집`을 눌렀을 때만 입력 필드·미리보기·저장/취소가 나타난다. 제목·설명 편집 중 `Escape`는 설정 닫기 대신 편집 취소로 동작한다. **사이트 정보** 카드는 로고, 공개 URL, 푸터 저작권 문구를 관리한다. **POST 설정**·**어나운스 바**는 읽기 모드에서도 토글 UI를 비활성화 상태로 보여 주며, 켜짐 상태도 조작 가능한 활성 스위치처럼 보이지 않도록 낮은 대비와 `cursor-not-allowed`를 사용한다. +- 관리자 사이트 설정 UI는 `/admin/settings`에서 제공한다. Ghost Admin과 유사하게 **전체 화면**으로 표시하며, 좌측 내비와 우측 본문을 **한 덩어리로 중앙 정렬**(`max-w` 래퍼, 본문 카드 영역은 약 760px 상한)하고, 페이지 배경은 밝은 회색·본문 열은 흰색으로 구분한다. 우측 본문을 스크롤하면 현재 보이는 구역에 맞춰 좌측 메뉴 활성 배경을 갱신한다. **우측 상단 고정** 닫기 버튼과 `Escape` 키로 설정 화면을 닫으며, 브라우저 히스토리가 있으면 `뒤로 가기`, 없으면 `/admin`으로 이동한다. 게시물 백업 도구는 `게시물 내보내기`와 `게시물 가져오기` 독립 카드로 표시한다. 내보내기 기본 화면은 제목·설명·내보내기 버튼만 표시하고, 상세 설정은 버튼을 눌렀을 때만 펼쳐진다. 내보내기 상세는 전체·특정년·특정월·직접 지정 범위, 목표 ZIP 용량, ZIP당 최대 게시물 수 지정을 제공한다. 내보내기 작업 목록은 작업이 있을 때만 내보내기 설정 카드 아래 별도 카드로 표시하며, 완료 작업은 만료일과 준비 완료 파일 선택 목록을 중심으로 표시하고 전체 선택·선택 파일 다운로드·실패 작업 재시도·실패 상세 오류·완료/실패 작업 삭제를 제공한다. 진행도 영역은 대기·생성 중 작업에서만 표시한다. 가져오기 기본 화면은 제목·설명·가져오기 버튼만 표시하고, 버튼을 누르면 ZIP 드롭존과 `적용` 버튼이 펼쳐진다. 가져오기는 파일 선택만으로 실행하지 않고 `적용` 버튼을 눌렀을 때만 Import API를 호출한다. 대기 중·생성 중 작업이 있으면 새 내보내기 요청 버튼을 비활성화한다. **스팸 필터**는 가입 금지 닉네임을 저장한다. **블로그 제목·설명** 카드는 기본적으로 사이트 이름·설명을 읽기 전용으로 보여 주고, `편집`을 눌렀을 때만 입력 필드·미리보기·저장/취소가 나타난다. 제목·설명 편집 중 `Escape`는 설정 닫기 대신 편집 취소로 동작한다. **사이트 정보** 카드는 로고, 공개 URL, 푸터 저작권 문구를 관리한다. **POST 설정**·**어나운스 바**는 읽기 모드에서도 토글 UI를 비활성화 상태로 보여 주며, 켜짐 상태도 조작 가능한 활성 스위치처럼 보이지 않도록 낮은 대비와 `cursor-not-allowed`를 사용한다. - 게시물 Import/Export 1차 포맷은 Obsidian 호환 백업 번들을 기준으로 한다. Export는 게시물마다 별도 폴더를 만들고, 폴더 안에 `제목.md` 메인 파일과 `images/`, `files/` 같은 자산 폴더를 함께 둔다. 본문은 기존 Markdown을 최대한 유지하되 `/uploads/...`로 연결된 내부 이미지·파일은 번들 안의 로컬 파일로 복사하고, Markdown 참조는 `./images/...` 또는 `./files/...` 같은 상대 경로로 재작성한다. 제목·슬러그·상태·발행일·요약·대표 이미지·SEO·태그는 YAML frontmatter에 저장한다. Import는 같은 구조의 ZIP을 읽어 frontmatter를 게시물 메타데이터로 복원하고, 로컬 자산은 `/uploads/posts/YYYY/MM/`에 새 파일로 저장한 뒤 본문 경로를 새 `/uploads/...` URL로 다시 매핑한다. 태그는 inline 배열(`tags: ["a"]`)과 Obsidian식 블록 배열(`tags:\n- a`)을 모두 읽는다. 같은 슬러그가 이미 있으면 기존 글을 덮어쓰지 않고 `slug-2`, `slug-3`처럼 새 슬러그로 가져온다. ZIP 안에서 자산을 찾지 못한 경우 Import는 계속 진행하되 응답 경고로 누락 경로를 알려 준다. 1회 Import는 최대 1000개 Markdown 게시물까지 처리한다. -- 대용량 게시물 Export는 즉시 응답 다운로드가 아니라 백그라운드 작업으로 처리한다. 관리자가 Export를 요청하면 서버는 작업 레코드를 만들고, 게시물을 목표 zip 용량 기준으로 나누어 여러 zip 파일을 순차 생성한다. 계획 단계에서는 본문 문자열 바이트와 내부 `/uploads` 자산 파일 크기를 합산해 `max_file_size_bytes`를 넘기기 전에 새 분할 파일을 만들며, `chunk_size`는 한 ZIP에 들어갈 게시물 수의 안전 상한으로만 사용한다. 현재 구현은 요청 시 `post_export_jobs` 작업과 `post_export_files` 분할 파일 계획을 생성하고, 서버 프로세스 안에서 대기 작업을 순차 실행해 `processed_count`, `current_part_index`, `progress_message`, `started_at`을 갱신한다. Export 대상은 전체 또는 `COALESCE(published_at, created_at)` 기준 특정년·특정월·직접 지정 날짜 범위로 제한할 수 있다. 설정 화면은 대기 중·생성 중 작업이 있을 때 5초 간격으로 작업 목록을 새로고침한다. 파일명은 기본적으로 `사이트명_범위_시작번호-끝번호.zip` 형식을 사용한다(예: `sori.studio_2026-05_1-100.zip`). 각 zip에는 해당 범위 게시물 폴더와 자산 폴더가 들어가며, 준비 완료된 분할 파일은 관리자 다운로드 API로 받을 수 있다. 준비 완료 파일은 일괄 다운로드 버튼으로 브라우저에서 순차 다운로드할 수 있다. 실패 작업은 이미 준비된 파일을 유지하고 실패·대기 파일만 다시 생성하도록 재시도할 수 있으며, 실패 상세 로그는 작업 카드에서 확인한다. Resend 환경 변수가 설정되어 있으면 모든 분할 파일 생성 완료 후 요청 관리자 이메일로 알림을 보낸다. 완료·실패한 작업은 관리자가 즉시 삭제할 수 있고, 삭제 시 DB 레코드와 생성 ZIP 파일을 함께 정리한다. 서버 용량 보호를 위해 export 산출물 보존 기간은 최대 100일로 제한하며, 만료된 완료·실패 작업은 목록 조회나 새 요청 시 생성 ZIP 파일과 함께 자동 정리된다. +- 대용량 게시물 Export는 즉시 응답 다운로드가 아니라 백그라운드 작업으로 처리한다. 관리자가 Export를 요청하면 서버는 작업 레코드를 만들고, 게시물을 목표 zip 용량 기준으로 나누어 여러 zip 파일을 순차 생성한다. 계획 단계에서는 본문 문자열 바이트와 내부 `/uploads` 자산 파일 크기를 합산해 `max_file_size_bytes`를 넘기기 전에 새 분할 파일을 만들며, `chunk_size`는 한 ZIP에 들어갈 게시물 수의 안전 상한으로만 사용한다. 현재 구현은 요청 시 `post_export_jobs` 작업과 `post_export_files` 분할 파일 계획을 생성하고, 서버 프로세스 안에서 대기 작업을 순차 실행해 `processed_count`, `current_part_index`, `progress_message`, `started_at`을 갱신한다. Export 대상은 전체 또는 `COALESCE(published_at, created_at)` 기준 특정년·특정월·직접 지정 날짜 범위로 제한할 수 있다. 설정 화면은 대기 중·생성 중 작업이 있을 때 5초 간격으로 작업 목록을 새로고침한다. 파일명은 기본적으로 `사이트명_범위_시작번호-끝번호.zip` 형식을 사용한다(예: `sori.studio_2026-05_1-100.zip`). 각 zip에는 해당 범위 게시물 폴더와 자산 폴더가 들어가며, 준비 완료된 분할 파일은 관리자 다운로드 API로 받을 수 있다. 준비 완료 파일은 체크박스로 선택한 뒤 전체 선택 또는 선택 파일 다운로드로 브라우저에서 순차 다운로드할 수 있다. 실패 작업은 이미 준비된 파일을 유지하고 실패·대기 파일만 다시 생성하도록 재시도할 수 있으며, 실패 상세 로그는 작업 카드에서 확인한다. Resend 환경 변수가 설정되어 있으면 모든 분할 파일 생성 완료 후 요청 관리자 이메일로 알림을 보낸다. 완료·실패한 작업은 관리자가 즉시 삭제할 수 있고, 삭제 시 DB 레코드와 생성 ZIP 파일을 함께 정리한다. 서버 용량 보호를 위해 export 산출물 보존 기간은 최대 100일로 제한하며, 만료된 완료·실패 작업은 목록 조회나 새 요청 시 생성 ZIP 파일과 함께 자동 정리된다. - 사이트 설정은 `site_settings` 테이블의 단일 레코드로 관리한다. - 관리자는 사이트 이름, 설명, 사이트 URL, 로고 이미지, 저작권 문구를 수정할 수 있다. - **메인 화면**(`home_cover_image_url`, `home_cover_dark_image_url`, `home_cover_title`, `home_cover_text`): 홈(`/`) 상단 720px 커버 배너. 라이트 이미지는 기본 커버이며, 다크 이미지가 있으면 시스템 다크모드 또는 `html[data-theme='dark']`에서 다크 이미지를 표시한다. 다크 이미지가 없으면 라이트 이미지를 그대로 사용한다. 이미지가 있을 때만 `HomeHero`를 표시하며, 제목·짧은 본문은 이미지 왼쪽 하단 그라데이션 오버레이로 겹친다. 오버레이 본문은 textarea에서 입력한 줄바꿈(`\n`)을 저장·표시하며, `HomeHero` 본문은 `whitespace-pre-line`으로 여러 줄을 렌더링한다. 관리자 UI에서는 라이트모드와 다크모드 프리뷰를 상하로 모두 표시하고, 각 모드 제목 오른쪽의 이미지 변경·삭제 버튼으로 개별 이미지를 관리한다. 이미지가 비어 있는 모드는 점선 드롭존으로 표시하며 파일 선택과 드래그 앤 드롭 업로드를 지원한다. 커버 파일 업로드·제목·본문은 편집 뒤 **저장** 한 번에 `PUT /admin/api/settings`로 반영한다. 파일 업로드 API는 디스크에만 올리고 URL을 돌려준다(가로 720px WebP, `/uploads/system/home-cover-YYYYMM-random.webp`). diff --git a/docs/update.md b/docs/update.md index 39d3889..4ba53cc 100644 --- a/docs/update.md +++ b/docs/update.md @@ -1,5 +1,11 @@ # 업데이트 이력 +## v1.5.32 + +- 관리자 사이트 설정: 최근 내보내기 작업 카드에서 요청일·목표 용량·분할 설정 표시를 제거하고 만료일만 표시하도록 정리. +- 관리자 사이트 설정: 완료된 내보내기 작업은 진행도 영역을 숨기고 대기·생성 중 작업에서만 진행도를 표시하도록 수정. +- 관리자 사이트 설정: 내보내기 파일 목록을 체크 선택 방식으로 바꾸고 전체 선택·선택 파일 다운로드 기능 추가. + ## v1.5.31 - 관리자 사이트 설정: 게시물 내보내기 요청 설정 카드와 최근 내보내기 작업 다운로드 카드를 분리. diff --git a/package-lock.json b/package-lock.json index 3e863b5..c9fd7da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sori.studio", - "version": "1.5.31", + "version": "1.5.32", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sori.studio", - "version": "1.5.31", + "version": "1.5.32", "hasInstallScript": true, "dependencies": { "@nuxtjs/tailwindcss": "^6.14.0", diff --git a/package.json b/package.json index 8488eeb..d454d15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sori.studio", - "version": "1.5.31", + "version": "1.5.32", "private": true, "type": "module", "imports": { diff --git a/pages/admin/settings/index.vue b/pages/admin/settings/index.vue index e35af5b..ef459d3 100644 --- a/pages/admin/settings/index.vue +++ b/pages/admin/settings/index.vue @@ -23,6 +23,7 @@ const uploadingHomeCoverDark = ref(false) const requestingPostExport = ref(false) const deletingPostExportJobIds = ref([]) const downloadingPostExportJobIds = ref([]) +const selectedPostExportFileIds = ref({}) const retryingPostExportJobIds = ref([]) const importingPosts = ref(false) const postImportFile = ref(null) @@ -716,7 +717,110 @@ const getReadyPostExportFiles = (job) => Array.isArray(job?.files) : [] /** - * Export 일괄 다운로드 중 여부 + * Export 작업의 선택된 파일 ID를 가져온다. + * @param {string} jobId - 작업 ID + * @returns {Array} 선택된 파일 ID 목록 + */ +const getSelectedPostExportFileIds = (jobId) => Array.isArray(selectedPostExportFileIds.value[jobId]) + ? selectedPostExportFileIds.value[jobId] + : [] + +/** + * Export 파일이 선택되었는지 확인한다. + * @param {Object} job - Export 작업 + * @param {Object} file - Export 파일 + * @returns {boolean} 선택 여부 + */ +const isPostExportFileSelected = (job, file) => getSelectedPostExportFileIds(job?.id).includes(file?.id) + +/** + * Export 파일 선택 상태를 변경한다. + * @param {Object} job - Export 작업 + * @param {Object} file - Export 파일 + * @param {boolean} selected - 선택 여부 + * @returns {void} + */ +const setPostExportFileSelected = (job, file, selected) => { + if (!job?.id || !file?.id || file.status !== 'ready' || !file.filePath) { + return + } + + const currentIds = getSelectedPostExportFileIds(job.id) + const nextIds = selected + ? [...new Set([...currentIds, file.id])] + : currentIds.filter((fileId) => fileId !== file.id) + + selectedPostExportFileIds.value = { + ...selectedPostExportFileIds.value, + [job.id]: nextIds + } +} + +/** + * Export 작업에서 선택된 다운로드 가능 파일을 가져온다. + * @param {Object} job - Export 작업 + * @returns {Array} 선택된 다운로드 가능 파일 목록 + */ +const getSelectedReadyPostExportFiles = (job) => { + const selectedIds = getSelectedPostExportFileIds(job?.id) + + return getReadyPostExportFiles(job).filter((file) => selectedIds.includes(file.id)) +} + +/** + * Export 작업의 모든 다운로드 가능 파일이 선택되었는지 확인한다. + * @param {Object} job - Export 작업 + * @returns {boolean} 전체 선택 여부 + */ +const areAllReadyPostExportFilesSelected = (job) => { + const readyFiles = getReadyPostExportFiles(job) + const selectedIds = getSelectedPostExportFileIds(job?.id) + + return readyFiles.length > 0 && readyFiles.every((file) => selectedIds.includes(file.id)) +} + +/** + * Export 작업의 다운로드 가능 파일 전체 선택을 전환한다. + * @param {Object} job - Export 작업 + * @returns {void} + */ +const toggleAllPostExportFiles = (job) => { + if (!job?.id) { + return + } + + const readyFiles = getReadyPostExportFiles(job) + const nextIds = areAllReadyPostExportFilesSelected(job) + ? [] + : readyFiles.map((file) => file.id) + + selectedPostExportFileIds.value = { + ...selectedPostExportFileIds.value, + [job.id]: nextIds + } +} + +/** + * Export 작업의 파일 선택을 비운다. + * @param {string} jobId - 작업 ID + * @returns {void} + */ +const clearSelectedPostExportFiles = (jobId) => { + selectedPostExportFileIds.value = { + ...selectedPostExportFileIds.value, + [jobId]: [] + } +} + +/** + * Export 진행도 영역 표시 여부를 확인한다. + * @param {Object} job - Export 작업 + * @returns {boolean} 진행도 표시 여부 + */ +const shouldShowPostExportProgress = (job) => job?.status === 'queued' || job?.status === 'processing' + +/** + * Export 다운로드 중 여부 * @param {string} jobId - 작업 ID * @returns {boolean} 다운로드 중 여부 */ @@ -730,22 +834,27 @@ const isDownloadingPostExportJob = (jobId) => downloadingPostExportJobIds.value. const isRetryingPostExportJob = (jobId) => retryingPostExportJobIds.value.includes(jobId) /** - * Export 분할 파일을 브라우저에서 순차 다운로드한다. + * 선택한 Export 분할 파일을 브라우저에서 순차 다운로드한다. * @param {Object} job - Export 작업 * @returns {Promise} */ const downloadPostExportJobFiles = async (job) => { - const readyFiles = getReadyPostExportFiles(job) + const selectedFiles = getSelectedReadyPostExportFiles(job) - if (!job?.id || readyFiles.length === 0 || isDownloadingPostExportJob(job.id)) { + if (!job?.id || isDownloadingPostExportJob(job.id)) { + return + } + + if (selectedFiles.length === 0) { + showToast('error', '다운로드할 파일을 선택해 주세요.') return } downloadingPostExportJobIds.value = [...downloadingPostExportJobIds.value, job.id] - showToast('info', `${readyFiles.length}개 파일을 순차 다운로드합니다.`) + showToast('info', `${selectedFiles.length}개 선택 파일을 순차 다운로드합니다.`) try { - for (const file of readyFiles) { + for (const file of selectedFiles) { const link = document.createElement('a') link.href = getPostExportDownloadUrl(file) link.download = file.fileName || '' @@ -755,7 +864,8 @@ const downloadPostExportJobFiles = async (job) => { link.remove() await new Promise((resolve) => window.setTimeout(resolve, 700)) } - showToast('success', '일괄 다운로드 요청을 보냈습니다.') + clearSelectedPostExportFiles(job.id) + showToast('success', '선택 파일 다운로드 요청을 보냈습니다.') } finally { downloadingPostExportJobIds.value = downloadingPostExportJobIds.value.filter((id) => id !== job.id) } @@ -2436,21 +2546,10 @@ onBeforeUnmount(() => {

- 요청: {{ formatPostDateTime(job.createdAt) }} · 만료: {{ formatPostDateTime(job.expiresAt) }} -

-

- 목표 용량 {{ formatExportFileSize(job.maxFileSizeBytes) }} · 최대 {{ job.chunkSize }}개/ZIP + 만료: {{ formatPostDateTime(job.expiresAt) }}

-
-
+
진행도 {{ getPostExportProgressLabel(job) }} @@ -2503,34 +2605,53 @@ onBeforeUnmount(() => {
+
+ + +
+

{{ file.fileName }}

- {{ file.postStart }}-{{ file.postEnd }} · {{ formatExportFileSize(file.fileSizeBytes) }} + {{ file.postStart }}-{{ file.postEnd }}

- - 다운로드 - - +