대표 이미지 선택 흐름 정리

This commit is contained in:
2026-05-07 16:02:50 +09:00
parent f757c3db78
commit 97d2d8ffb3
7 changed files with 58 additions and 22 deletions

View File

@@ -47,6 +47,7 @@ const isRestoringAutosave = ref(false)
const isSettingsOpen = ref(true) const isSettingsOpen = ref(true)
const tagInput = ref('') const tagInput = ref('')
const activeMediaPickerTab = ref('upload') const activeMediaPickerTab = ref('upload')
const selectedMediaPickerUrl = ref('')
/** /**
* ISO 날짜를 datetime-local 입력값으로 변환 * ISO 날짜를 datetime-local 입력값으로 변환
@@ -337,6 +338,7 @@ const fetchMediaItems = async () => {
const openMediaPicker = async (target = 'featuredImage') => { const openMediaPicker = async (target = 'featuredImage') => {
mediaPickerTarget.value = target mediaPickerTarget.value = target
activeMediaPickerTab.value = 'upload' activeMediaPickerTab.value = 'upload'
selectedMediaPickerUrl.value = form[target] || ''
isMediaPickerOpen.value = true isMediaPickerOpen.value = true
await fetchMediaItems() await fetchMediaItems()
} }
@@ -350,12 +352,24 @@ const closeMediaPicker = () => {
} }
/** /**
* 대표 이미지 선택 * 대표 이미지 선택 상태 변경
* @param {Object} item - 미디어 항목 * @param {Object} item - 미디어 항목
* @returns {void} * @returns {void}
*/ */
const selectPickedImage = (item) => { const selectPickedImage = (item) => {
form[mediaPickerTarget.value] = item.url selectedMediaPickerUrl.value = item.url
}
/**
* 선택한 대표 이미지 적용
* @returns {void}
*/
const applyPickedImage = () => {
if (!selectedMediaPickerUrl.value) {
return
}
form[mediaPickerTarget.value] = selectedMediaPickerUrl.value
closeMediaPicker() closeMediaPicker()
} }
@@ -425,9 +439,9 @@ const uploadFeaturedImageFile = async (file) => {
method: 'POST', method: 'POST',
body: formData body: formData
}) })
form.featuredImage = result.files?.[0]?.url || '' selectedMediaPickerUrl.value = result.files?.[0]?.url || ''
await fetchMediaItems() await fetchMediaItems()
closeMediaPicker() activeMediaPickerTab.value = 'library'
} finally { } finally {
isUploadingFeaturedImage.value = false isUploadingFeaturedImage.value = false
} }
@@ -586,17 +600,13 @@ defineExpose({
<main class="admin-post-form__editor-scroll min-h-0 flex-1 overflow-y-auto"> <main class="admin-post-form__editor-scroll min-h-0 flex-1 overflow-y-auto">
<section class="admin-post-form__content mx-auto w-full max-w-[804px] px-8 pb-16 pt-32"> <section class="admin-post-form__content mx-auto w-full max-w-[804px] px-8 pb-16 pt-32">
<div class="admin-post-form__feature-block mb-9"> <div class="admin-post-form__feature-block mb-9">
<figure v-if="form.featuredImage" class="admin-post-form__featured-editor overflow-hidden rounded border border-line bg-white"> <figure v-if="form.featuredImage" class="admin-post-form__featured-editor group relative overflow-hidden bg-white">
<img class="admin-post-form__featured-editor-image aspect-[16/9] w-full bg-surface object-cover" :src="form.featuredImage" alt=""> <img class="admin-post-form__featured-editor-image aspect-[16/9] w-full bg-surface object-cover" :src="form.featuredImage" alt="">
<figcaption class="admin-post-form__featured-editor-actions flex flex-wrap items-center gap-2 border-t border-line p-3"> <figcaption class="admin-post-form__featured-editor-actions pointer-events-none absolute inset-0 flex items-end justify-end gap-2 bg-gradient-to-t from-black/40 via-black/5 to-transparent p-4 opacity-0 transition-opacity duration-150 group-hover:pointer-events-auto group-hover:opacity-100 group-focus-within:pointer-events-auto group-focus-within:opacity-100">
<button class="admin-post-form__featured-change rounded border border-line px-3 py-1.5 text-xs font-semibold transition-colors hover:border-[#c8ced3] hover:bg-[#eff1f2]" type="button" @click="openMediaPicker('featuredImage')"> <button class="admin-post-form__featured-change rounded bg-white/95 px-3 py-1.5 text-xs font-semibold text-[#15171a] shadow-sm transition-colors hover:bg-white focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white" type="button" @click="openMediaPicker('featuredImage')">
변경 변경
</button> </button>
<label class="admin-post-form__featured-reupload cursor-pointer rounded border border-line px-3 py-1.5 text-xs font-semibold transition-colors hover:border-[#c8ced3] hover:bg-[#eff1f2]"> <button class="admin-post-form__featured-remove rounded bg-[#fff1f2]/95 px-3 py-1.5 text-xs font-semibold text-red-700 shadow-sm transition-colors hover:bg-white focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white" type="button" @click="removeFeaturedImage">
업로드
<input class="sr-only" type="file" accept="image/*" @change="uploadFeaturedImage">
</label>
<button class="admin-post-form__featured-remove rounded border border-red-200 px-3 py-1.5 text-xs font-semibold text-red-700 transition-colors hover:bg-red-50" type="button" @click="removeFeaturedImage">
삭제 삭제
</button> </button>
</figcaption> </figcaption>
@@ -874,12 +884,19 @@ defineExpose({
<button <button
v-for="item in mediaItems" v-for="item in mediaItems"
:key="item.url" :key="item.url"
class="admin-post-form__media-picker-item overflow-hidden border border-line bg-white text-left transition hover:border-[#8e9cac] hover:shadow-sm" class="admin-post-form__media-picker-item relative overflow-hidden border bg-white text-left transition hover:border-[#8e9cac] hover:shadow-sm"
:class="selectedMediaPickerUrl === item.url ? 'border-[#15171a] ring-2 ring-[#15171a]' : 'border-line'"
type="button" type="button"
:aria-pressed="selectedMediaPickerUrl === item.url"
@click="selectPickedImage(item)" @click="selectPickedImage(item)"
> >
<img class="admin-post-form__media-picker-image aspect-square w-full bg-surface object-cover" :src="item.url" :alt="item.title"> <img class="admin-post-form__media-picker-image aspect-square w-full bg-surface object-cover" :src="item.url" :alt="item.title">
<span class="admin-post-form__media-picker-name block truncate px-2 py-1.5 text-xs font-semibold text-ink">{{ item.name }}</span> <span class="admin-post-form__media-picker-name block truncate px-2 py-1.5 text-xs font-semibold text-ink">{{ item.name }}</span>
<span v-if="selectedMediaPickerUrl === item.url" class="admin-post-form__media-picker-selected absolute right-2 top-2 grid size-6 place-items-center rounded-full bg-[#15171a] text-white" aria-hidden="true">
<svg width="13" height="10" viewBox="0 0 13 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 5L4.5 8.5L12 1" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</span>
</button> </button>
</div> </div>
<p v-else class="admin-post-form__media-picker-empty border border-dashed border-line p-8 text-center text-sm text-muted"> <p v-else class="admin-post-form__media-picker-empty border border-dashed border-line p-8 text-center text-sm text-muted">
@@ -889,11 +906,12 @@ defineExpose({
</div> </div>
<div class="admin-post-form__media-picker-footer flex h-14 shrink-0 items-center justify-end border-t border-line px-5"> <div class="admin-post-form__media-picker-footer flex h-14 shrink-0 items-center justify-end border-t border-line px-5">
<button <button
class="admin-post-form__media-picker-confirm h-9 rounded border border-line px-4 text-sm font-semibold text-muted disabled:cursor-not-allowed disabled:opacity-50" class="admin-post-form__media-picker-confirm h-9 rounded bg-[#15171a] px-4 text-sm font-semibold text-white transition-colors hover:bg-black disabled:cursor-not-allowed disabled:bg-[#d7dce0] disabled:text-[#8e9cac]"
type="button" type="button"
disabled :disabled="!selectedMediaPickerUrl"
@click="applyPickedImage"
> >
대표 이미지 설정 대표 이미지 적용
</button> </button>
</div> </div>
</section> </section>

View File

@@ -1,5 +1,13 @@
# 의사결정 이력 # 의사결정 이력
## 2026-05-07 v0.0.43
### 대표 이미지 액션과 선택 확정 흐름 결정
대표 이미지가 이미 설정된 상태의 변경과 삭제 액션은 이미지 아래 별도 영역이 아니라 이미지 hover 오버레이로 표시한다. 실제 공개 화면에서 보일 이미지 비율과 편집용 버튼 영역이 섞이면 작성자가 레이아웃을 잘못 인식할 수 있기 때문이다.
대표 이미지 선택 모달에서는 미디어 클릭 즉시 값을 바꾸지 않고 선택 상태만 표시한 뒤, 하단 대표 이미지로 적용 버튼으로 확정한다. 변경 작업은 실수했을 때 되돌리기보다 확정 전 확인이 더 안전한 흐름이기 때문이다.
## 2026-05-07 v0.0.42 ## 2026-05-07 v0.0.42
### 태그 입력과 대표 이미지 선택 흐름 결정 ### 태그 입력과 대표 이미지 선택 흐름 결정

View File

@@ -26,7 +26,7 @@
| 파일 | 화면 위치 | | 파일 | 화면 위치 |
|------|-----------| |------|-----------|
| components/admin/AdminPostForm.vue | 관리자 글 작성/수정 폼, Ghost 스타일 전체 화면 에디터, 좌우 분할 설정 패널, 설정 패널 전환 애니메이션, Post URL 보기 액션, 하단 삭제 액션, 대표 이미지 업로드/미디어 선택, 배지형 태그 입력, 로컬 자동 저장, 예약 발행 시각 입력, SEO 설정, 미리보기 요청 | | components/admin/AdminPostForm.vue | 관리자 글 작성/수정 폼, Ghost 스타일 전체 화면 에디터, 좌우 분할 설정 패널, 설정 패널 전환 애니메이션, Post URL 보기 액션, 하단 삭제 액션, 대표 이미지 hover 액션과 업로드/미디어 선택 확정, 배지형 태그 입력, 로컬 자동 저장, 예약 발행 시각 입력, SEO 설정, 미리보기 요청 |
| components/admin/AdminPageForm.vue | 관리자 페이지 작성/수정 폼, 대표 이미지 선택 | | components/admin/AdminPageForm.vue | 관리자 페이지 작성/수정 폼, 대표 이미지 선택 |
| components/admin/AdminBlockEditor.vue | 관리자 글 블록형 에디터, 이미지/갤러리/콜아웃/토글/임베드 블록, 한글 조합 입력 처리, Shift+Enter 줄바꿈, 코드 블록 단축 변환, AFFiNE 참고 세로 막대형 블록 핸들 선택/삭제/드래그 이동과 삽입선 표시, 하단 빈 입력 블록 유지, 본문 placeholder 표시 | | components/admin/AdminBlockEditor.vue | 관리자 글 블록형 에디터, 이미지/갤러리/콜아웃/토글/임베드 블록, 한글 조합 입력 처리, Shift+Enter 줄바꿈, 코드 블록 단축 변환, AFFiNE 참고 세로 막대형 블록 핸들 선택/삭제/드래그 이동과 삽입선 표시, 하단 빈 입력 블록 유지, 본문 placeholder 표시 |
| components/admin/AdminTagForm.vue | 관리자 태그 생성/수정 폼 | | components/admin/AdminTagForm.vue | 관리자 태그 생성/수정 폼 |

View File

@@ -306,6 +306,7 @@ components/content/
- 공개 가능한 글의 보기 액션은 게시물 설정 패널의 Post URL 라벨 오른쪽에 표시한다. - 공개 가능한 글의 보기 액션은 게시물 설정 패널의 Post URL 라벨 오른쪽에 표시한다.
- 글 삭제 액션은 게시물 설정 패널 하단의 빨간 outline 버튼으로 제공한다. - 글 삭제 액션은 게시물 설정 패널 하단의 빨간 outline 버튼으로 제공한다.
- 대표 이미지는 본문 제목 위의 에디터 흐름 안에서 추가, 변경, 삭제한다. - 대표 이미지는 본문 제목 위의 에디터 흐름 안에서 추가, 변경, 삭제한다.
- 대표 이미지가 설정된 상태의 변경/삭제 액션은 실제 이미지 레이아웃을 바꾸지 않도록 이미지 hover 또는 focus 오버레이로 표시한다.
- 대표 이미지 선택 모달은 이미지 업로드 탭과 미디어 라이브러리 탭을 제공한다. - 대표 이미지 선택 모달은 이미지 업로드 탭과 미디어 라이브러리 탭을 제공한다.
- 제목 입력에서 Enter를 누르면 본문 첫 블록으로 포커스를 이동한다. - 제목 입력에서 Enter를 누르면 본문 첫 블록으로 포커스를 이동한다.
- 새 글 작성처럼 본문이 비어 있는 경우에도 빈 문단 블록을 먼저 생성한다. - 새 글 작성처럼 본문이 비어 있는 경우에도 빈 문단 블록을 먼저 생성한다.
@@ -322,7 +323,8 @@ components/content/
- 관리자 작성 화면과 공개 본문은 같은 마크다운 렌더링 기준을 사용한다. - 관리자 작성 화면과 공개 본문은 같은 마크다운 렌더링 기준을 사용한다.
- 태그 입력은 배지형 입력으로 제공하며 Enter 또는 쉼표 입력 시 태그를 추가하고, 배지의 x 버튼으로 삭제한다. - 태그 입력은 배지형 입력으로 제공하며 Enter 또는 쉼표 입력 시 태그를 추가하고, 배지의 x 버튼으로 삭제한다.
- 대표 이미지는 URL 직접 입력이 아니라 미디어 선택 또는 이미지 업로드 탭으로 설정한다. - 대표 이미지는 URL 직접 입력이 아니라 미디어 선택 또는 이미지 업로드 탭으로 설정한다.
- 대표 이미지가 설정되면 관리자 글 폼에 썸네일과 삭제/변경 액션을 표시한다. - 대표 이미지 선택 모달에서 미디어를 클릭하면 선택 상태만 표시하고, 하단 대표 이미지로 적용 버튼을 눌렀을 때 글 폼에 반영한다.
- 대표 이미지가 설정되면 관리자 글 폼에 썸네일과 hover 오버레이 삭제/변경 액션을 표시한다.
- 글 SEO 설정은 SEO 제목, SEO 설명, 검색엔진 노출 제외 여부를 저장한다. - 글 SEO 설정은 SEO 제목, SEO 설명, 검색엔진 노출 제외 여부를 저장한다.
- Canonical URL은 별도 입력을 받지 않고 기본 글 주소를 사용한다. - Canonical URL은 별도 입력을 받지 않고 기본 글 주소를 사용한다.
- 공개 게시물 상세 화면은 SEO 제목이 없으면 글 제목, SEO 설명이 없으면 요약을 메타 태그 기본값으로 사용한다. - 공개 게시물 상세 화면은 SEO 제목이 없으면 글 제목, SEO 설명이 없으면 요약을 메타 태그 기본값으로 사용한다.
@@ -454,6 +456,6 @@ APP_PORT=43118
## 버전 관리 ## 버전 관리
- 현재 버전: v0.0.42 - 현재 버전: v0.0.43
- 첫 커밋 이후 변경사항을 커밋할 때마다 패치 버전 증가 - 첫 커밋 이후 변경사항을 커밋할 때마다 패치 버전 증가
- 메이저/마이너 버전은 구조 변경 또는 기능 묶음 단위로 결정 - 메이저/마이너 버전은 구조 변경 또는 기능 묶음 단위로 결정

View File

@@ -1,5 +1,13 @@
# 업데이트 이력 # 업데이트 이력
## v0.0.43
- 대표 이미지가 설정된 상태의 변경/삭제 액션을 이미지 아래 버튼 영역이 아니라 이미지 hover 오버레이로 수정.
- 대표 이미지 선택 모달에서 미디어 클릭 시 즉시 적용하지 않고 선택 상태만 표시하도록 수정.
- 대표 이미지 선택 모달 하단의 대표 이미지로 적용 버튼으로 선택 이미지를 확정하도록 수정.
- 기술 명세 현재 버전을 v0.0.43으로 갱신.
- 패키지 버전을 0.0.43으로 갱신.
## v0.0.42 ## v0.0.42
- 관리자 글쓰기 화면의 문서 스크롤 잠금 클래스를 html/body에 직접 적용하도록 보강. - 관리자 글쓰기 화면의 문서 스크롤 잠금 클래스를 html/body에 직접 적용하도록 보강.

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "sori.studio", "name": "sori.studio",
"version": "0.0.42", "version": "0.0.43",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "sori.studio", "name": "sori.studio",
"version": "0.0.42", "version": "0.0.43",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@nuxtjs/tailwindcss": "^6.14.0", "@nuxtjs/tailwindcss": "^6.14.0",

View File

@@ -1,6 +1,6 @@
{ {
"name": "sori.studio", "name": "sori.studio",
"version": "0.0.42", "version": "0.0.43",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {