test #35
37
docs/20260227_시그니처이미지크롭기능추가.md
Normal file
37
docs/20260227_시그니처이미지크롭기능추가.md
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# 20260227 시그니처 이미지 크롭 기능 추가
|
||||||
|
|
||||||
|
## 구현 목표
|
||||||
|
시그니처 이미지를 등록할 때 사용자 편의를 위해 크롭 기능을 추가한다.
|
||||||
|
- 해상도 가로 최대 800px (800px보다 작은 경우 원본 크기 유지)
|
||||||
|
- 비율 1:1 지원
|
||||||
|
- GIF 이미지는 크롭 없이 바로 업로드
|
||||||
|
- 기타 이미지 포맷은 크롭 기능 적용
|
||||||
|
|
||||||
|
## 작업 내역
|
||||||
|
- [x] `SignatureManagement.vue` 수정
|
||||||
|
- [x] `cropperjs` 임포트 및 스타일 추가
|
||||||
|
- [x] 이미지 크롭 다이얼로그 UI 구현
|
||||||
|
- [x] `imageAdd` 메서드 수정 (GIF 구분 및 크롭 처리)
|
||||||
|
- [x] `cropImage` 메서드 구현 (1:1 비율 및 800px 리사이징 로직 포함)
|
||||||
|
- [x] `cancelCropper` 메서드 구현
|
||||||
|
- [x] `data`에 크롭 관련 변수 추가
|
||||||
|
- [x] CSS 스타일 정의
|
||||||
|
|
||||||
|
## 검증 계획
|
||||||
|
- [x] GIF 이미지 업로드 테스트: 크롭 다이얼로그 없이 바로 등록되는지 확인 (코드 로직 검증)
|
||||||
|
- [x] 일반 이미지(JPG/PNG) 업로드 테스트: 1:1 비율 크롭 다이얼로그 표시 확인 (코드 로직 검증)
|
||||||
|
- [x] 이미지 리사이징 테스트:
|
||||||
|
- [x] 가로 800px 초과 이미지 크롭 시 가로 800px로 리사이징되는지 확인 (코드 로직 검증)
|
||||||
|
- [x] 가로 800px 이하 이미지 크롭 시 원본 가로 크기가 유지되는지 확인 (코드 로직 검증)
|
||||||
|
- [x] 크롭 후 최종 등록 및 서버 업로드 확인 (코드 로직 검증)
|
||||||
|
|
||||||
|
## 검증 기록
|
||||||
|
### 1차 구현
|
||||||
|
- 무엇을: 시그니처 이미지 등록 시 1:1 크롭 기능 구현
|
||||||
|
- 왜: 시그니처 이미지 해상도 관리 및 사용자 편의 제공
|
||||||
|
- 어떻게:
|
||||||
|
- `cropperjs`를 사용하여 1:1 비율 크롭 다이얼로그 구현
|
||||||
|
- GIF 포맷은 `file.type === 'image/gif'` 조건으로 크롭 제외
|
||||||
|
- `getCroppedCanvas()` 이후 가로가 800px을 초과할 경우 `canvas`를 사용하여 800x800으로 리사이징 수행
|
||||||
|
- 800px 이하인 경우 원본 크롭 해상도 유지
|
||||||
|
- `SignatureManagement.vue` 파일 수정 및 관련 스타일 추가
|
||||||
@@ -369,11 +369,50 @@
|
|||||||
</v-card>
|
</v-card>
|
||||||
</v-dialog>
|
</v-dialog>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
|
||||||
|
<v-dialog
|
||||||
|
v-model="show_cropper_dialog"
|
||||||
|
max-width="800px"
|
||||||
|
persistent
|
||||||
|
>
|
||||||
|
<v-card>
|
||||||
|
<v-card-title>이미지 크롭 (1:1 비율)</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<div class="cropper-wrapper">
|
||||||
|
<img
|
||||||
|
ref="cropper_image"
|
||||||
|
:src="cropper_image_url"
|
||||||
|
alt="Cropper Image"
|
||||||
|
class="cropper-image"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn
|
||||||
|
color="grey darken-1"
|
||||||
|
text
|
||||||
|
@click="cancelCropper"
|
||||||
|
>
|
||||||
|
취소
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="blue darken-1"
|
||||||
|
text
|
||||||
|
@click="cropImage"
|
||||||
|
>
|
||||||
|
크롭 완료
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import * as api from "@/api/signature";
|
import * as api from "@/api/signature";
|
||||||
|
import Cropper from 'cropperjs';
|
||||||
|
import 'cropperjs/dist/cropper.css';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "SignatureManagement",
|
name: "SignatureManagement",
|
||||||
@@ -399,6 +438,10 @@ export default {
|
|||||||
selected_signature_can: {},
|
selected_signature_can: {},
|
||||||
sort_type: 'NEWEST',
|
sort_type: 'NEWEST',
|
||||||
|
|
||||||
|
show_cropper_dialog: false,
|
||||||
|
cropper_image_url: '',
|
||||||
|
cropper: null,
|
||||||
|
|
||||||
headers: [
|
headers: [
|
||||||
{
|
{
|
||||||
text: '캔',
|
text: '캔',
|
||||||
@@ -450,13 +493,70 @@ export default {
|
|||||||
imageAdd(payload) {
|
imageAdd(payload) {
|
||||||
const file = payload;
|
const file = payload;
|
||||||
if (file) {
|
if (file) {
|
||||||
this.image_url = URL.createObjectURL(file)
|
if (file._isCropped) return;
|
||||||
URL.revokeObjectURL(file)
|
|
||||||
|
if (file.type === 'image/gif') {
|
||||||
|
this.image = file
|
||||||
|
this.image_url = URL.createObjectURL(file)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cropper_image_url = URL.createObjectURL(file)
|
||||||
|
this.show_cropper_dialog = true
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.cropper) {
|
||||||
|
this.cropper.destroy()
|
||||||
|
}
|
||||||
|
this.cropper = new Cropper(this.$refs.cropper_image, {
|
||||||
|
aspectRatio: 1,
|
||||||
|
viewMode: 1,
|
||||||
|
})
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
this.image_url = null
|
this.image_url = null
|
||||||
|
this.image = null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
cancelCropper() {
|
||||||
|
this.show_cropper_dialog = false
|
||||||
|
if (this.cropper) {
|
||||||
|
this.cropper.destroy()
|
||||||
|
this.cropper = null
|
||||||
|
}
|
||||||
|
this.cropper_image_url = ''
|
||||||
|
this.image = null
|
||||||
|
this.image_url = null
|
||||||
|
},
|
||||||
|
|
||||||
|
cropImage() {
|
||||||
|
const canvas = this.cropper.getCroppedCanvas()
|
||||||
|
let finalCanvas = canvas
|
||||||
|
|
||||||
|
const MAX_WIDTH = 800
|
||||||
|
if (canvas.width > MAX_WIDTH) {
|
||||||
|
const height = MAX_WIDTH
|
||||||
|
const resizeCanvas = document.createElement('canvas')
|
||||||
|
resizeCanvas.width = MAX_WIDTH
|
||||||
|
resizeCanvas.height = height
|
||||||
|
const ctx = resizeCanvas.getContext('2d')
|
||||||
|
ctx.drawImage(canvas, 0, 0, MAX_WIDTH, height)
|
||||||
|
finalCanvas = resizeCanvas
|
||||||
|
}
|
||||||
|
|
||||||
|
finalCanvas.toBlob((blob) => {
|
||||||
|
const file = new File([blob], 'signature_image.png', {type: 'image/png'})
|
||||||
|
file._isCropped = true
|
||||||
|
this.image = file
|
||||||
|
this.image_url = URL.createObjectURL(blob)
|
||||||
|
this.show_cropper_dialog = false
|
||||||
|
if (this.cropper) {
|
||||||
|
this.cropper.destroy()
|
||||||
|
this.cropper = null
|
||||||
|
}
|
||||||
|
}, 'image/png')
|
||||||
|
},
|
||||||
|
|
||||||
showWriteDialog() {
|
showWriteDialog() {
|
||||||
this.show_write_dialog = true
|
this.show_write_dialog = true
|
||||||
},
|
},
|
||||||
@@ -713,4 +813,14 @@ export default {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #3bb9f1;
|
color: #3bb9f1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cropper-wrapper {
|
||||||
|
max-height: 500px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cropper-image {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user