feat: TipTap 이미지 적용
This commit is contained in:
@@ -1,111 +0,0 @@
|
|||||||
<?php
|
|
||||||
namespace sammo;
|
|
||||||
|
|
||||||
include 'lib.php';
|
|
||||||
include 'func.php';
|
|
||||||
|
|
||||||
WebUtil::requireAJAX();
|
|
||||||
|
|
||||||
$session = Session::requireGameLogin([])->setReadOnly();
|
|
||||||
$userID = $session::getUserID();
|
|
||||||
$serverID = UniqueConst::$serverID;
|
|
||||||
|
|
||||||
$image = $_FILES['img'];
|
|
||||||
switch ($image['error']) {
|
|
||||||
case UPLOAD_ERR_OK:
|
|
||||||
break;
|
|
||||||
case UPLOAD_ERR_NO_FILE:
|
|
||||||
Json::die([
|
|
||||||
'result'=>false,
|
|
||||||
'reason'=>'파일이 없습니다.'
|
|
||||||
]);
|
|
||||||
case UPLOAD_ERR_INI_SIZE:
|
|
||||||
case UPLOAD_ERR_FORM_SIZE:
|
|
||||||
Json::die([
|
|
||||||
'result'=>false,
|
|
||||||
'reason'=>'업로드된 파일이 지나치게 큽니다.'
|
|
||||||
]);
|
|
||||||
default:
|
|
||||||
Json::die([
|
|
||||||
'result'=>false,
|
|
||||||
'reason'=>'업로드되지 않았습니다.'
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!is_uploaded_file($image['tmp_name'])) {
|
|
||||||
Json::die([
|
|
||||||
'result'=>false,
|
|
||||||
'reason'=>'제대로 파일이 업로드되지 않았습니다.'
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if($image['size'] > 1048576) {
|
|
||||||
//파일크기 검사
|
|
||||||
Json::die([
|
|
||||||
'result'=>false,
|
|
||||||
'reason'=>'1MB 이하로 올려주세요!'
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$size = getImageSize($image['tmp_name']);
|
|
||||||
|
|
||||||
$imageType = $size[2];
|
|
||||||
$availableImageType = array('.jpg'=>IMAGETYPE_JPEG, '.png'=>IMAGETYPE_PNG, '.gif'=>IMAGETYPE_GIF);
|
|
||||||
$newExt = array_search($imageType, $availableImageType, true);
|
|
||||||
|
|
||||||
if(!$newExt) {
|
|
||||||
Json::die([
|
|
||||||
'result'=>false,
|
|
||||||
'reason'=>'jpg, gif, png 파일이 아닙니다!'
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$db = RootDB::db();
|
|
||||||
$imgStor = KVStorage::getStorage($db, 'img_storage');
|
|
||||||
|
|
||||||
$picName = hash_file('md5', $image['tmp_name']);
|
|
||||||
$newPicName = "$picName$newExt";
|
|
||||||
|
|
||||||
$destDir = AppConf::getUserIconPathFS().'/uploaded_image';
|
|
||||||
$dest = $destDir.'/'.$newPicName;
|
|
||||||
|
|
||||||
if(!file_exists($dest)){
|
|
||||||
if (!file_exists($destDir)) {
|
|
||||||
mkdir($destDir);
|
|
||||||
}
|
|
||||||
if(!is_dir($destDir)) {
|
|
||||||
Json::die([
|
|
||||||
'result'=>false,
|
|
||||||
'reason'=>'버그! 업로드 경로 확인!'
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
if(!is_writable($destDir)){
|
|
||||||
Json::die([
|
|
||||||
'result'=>false,
|
|
||||||
'reason'=>'버그! 업로드 권한 확인!'
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$dest = $destDir.'/'.$newPicName;
|
|
||||||
|
|
||||||
if(!move_uploaded_file($image['tmp_name'], $dest)) {
|
|
||||||
Json::die([
|
|
||||||
'result'=>false,
|
|
||||||
'reason'=>'업로드에 실패했습니다!'
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$storedStatus = $imgStor->$newPicName??[];
|
|
||||||
$imgKey = "$serverID:$userID";
|
|
||||||
if(!key_exists($imgKey, $storedStatus)){
|
|
||||||
$storedStatus[$imgKey] = TimeUtil::now();
|
|
||||||
}
|
|
||||||
|
|
||||||
$imgStor->$newPicName = $storedStatus;
|
|
||||||
|
|
||||||
Json::die([
|
|
||||||
'result'=>true,
|
|
||||||
'reason'=>'성공',
|
|
||||||
'path'=>AppConf::getUserIconPathWeb().'/uploaded_image/'.$newPicName
|
|
||||||
]);
|
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace sammo\API\Misc;
|
||||||
|
|
||||||
|
use sammo\Session;
|
||||||
|
use DateTimeInterface;
|
||||||
|
use sammo\AppConf;
|
||||||
|
use sammo\KVStorage;
|
||||||
|
use sammo\RootDB;
|
||||||
|
use sammo\TimeUtil;
|
||||||
|
use sammo\UniqueConst;
|
||||||
|
use sammo\Validator;
|
||||||
|
|
||||||
|
class UploadImage extends \sammo\BaseAPI
|
||||||
|
{
|
||||||
|
public function validateArgs(): ?string
|
||||||
|
{
|
||||||
|
$v = new Validator($this->args);
|
||||||
|
$v->rule('required', [
|
||||||
|
'imageData',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!$v->validate()) {
|
||||||
|
return $v->errorStr();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRequiredSessionMode(): int
|
||||||
|
{
|
||||||
|
return static::REQ_GAME_LOGIN | static::REQ_READ_ONLY;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function launch(Session $session, ?DateTimeInterface $modifiedSince, ?string $reqEtag)
|
||||||
|
{
|
||||||
|
$imageData = base64_decode($this->args['imageData'], true);
|
||||||
|
if ($imageData === false) {
|
||||||
|
return "올바른 데이터가 아닙니다.";
|
||||||
|
}
|
||||||
|
|
||||||
|
if(strlen($imageData) > 1024*1024){
|
||||||
|
//NOTE: 가변 길이를 적용해야할까?
|
||||||
|
return "이미지 크기가 1MB보다 큽니다.";
|
||||||
|
}
|
||||||
|
|
||||||
|
$contentType = (new \finfo(FILEINFO_MIME_TYPE))->buffer($imageData);
|
||||||
|
if (substr($contentType, 0, 5) !== 'image') {
|
||||||
|
return '이미지 파일이 아닙니다: ' . $contentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
$extension = ltrim($contentType, 'image/');
|
||||||
|
$validExtensions = ['png', 'jpeg', 'jpg', 'gif', 'webp'];
|
||||||
|
if (!in_array(strtolower($extension), $validExtensions)) {
|
||||||
|
return '지원하지 않는 이미지 파일입니다: ' . $contentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
$oMD = hash_init('md5');
|
||||||
|
hash_update($oMD, $imageData);
|
||||||
|
$imgName = hash_final($oMD);
|
||||||
|
$imgFullName = "{$imgName}.{$extension}";
|
||||||
|
|
||||||
|
$destDir = AppConf::getUserIconPathFS() . '/uploaded_image';
|
||||||
|
$destPath = "{$destDir}/{$imgFullName}";
|
||||||
|
|
||||||
|
if (!file_exists($destPath)) {
|
||||||
|
if (!file_exists($destDir)) {
|
||||||
|
mkdir($destDir);
|
||||||
|
}
|
||||||
|
if (!is_dir($destDir)) {
|
||||||
|
return '버그! 업로드 경로 확인!';
|
||||||
|
}
|
||||||
|
if (!is_writable($destDir)) {
|
||||||
|
return '버그! 업로드 권한 확인!';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!file_put_contents($destPath, $imageData)) {
|
||||||
|
return '업로드에 실패했습니다!';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$db = RootDB::db();
|
||||||
|
$imgStor = KVStorage::getStorage($db, 'img_storage');
|
||||||
|
|
||||||
|
|
||||||
|
$userID = $session->userID;
|
||||||
|
$serverID = UniqueConst::$serverID;
|
||||||
|
|
||||||
|
$storedStatus = $imgStor->$imgFullName ?? [];
|
||||||
|
$imgKey = "$serverID:$userID";
|
||||||
|
if (!key_exists($imgKey, $storedStatus)) {
|
||||||
|
$storedStatus[$imgKey] = TimeUtil::now();
|
||||||
|
}
|
||||||
|
|
||||||
|
$imgStor->$imgFullName = $storedStatus;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'result' => true,
|
||||||
|
'path'=>AppConf::getUserIconPathWeb().'/uploaded_image/'.$imgFullName,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
.custom-image-original {
|
||||||
|
max-width: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-image-large {
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-image-medium {
|
||||||
|
width: 50%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-image-small {
|
||||||
|
margin: auto;
|
||||||
|
width: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-image-align-float-left{
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-image-align-float-right{
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-image-align-center{
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
}
|
||||||
|
.custom-image-align-left{
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: auto;
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-image-align-right{
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
@@ -48,6 +48,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import "@scss/dipcenter.scss";
|
import "@scss/dipcenter.scss";
|
||||||
import "@scss/common_legacy.scss";
|
import "@scss/common_legacy.scss";
|
||||||
|
import "@scss/editor_component.scss";
|
||||||
import TipTap from "./components/TipTap.vue";
|
import TipTap from "./components/TipTap.vue";
|
||||||
import { defineComponent } from "vue";
|
import { defineComponent } from "vue";
|
||||||
import { sammoAPI } from "./util/sammoAPI";
|
import { sammoAPI } from "./util/sammoAPI";
|
||||||
|
|||||||
@@ -76,7 +76,9 @@
|
|||||||
|
|
||||||
<b-button-group class="mx-1">
|
<b-button-group class="mx-1">
|
||||||
<b-button
|
<b-button
|
||||||
@click="editor.chain().focus().unsetColor().unsetBackgroundColor().run()"
|
@click="
|
||||||
|
editor.chain().focus().unsetColor().unsetBackgroundColor().run()
|
||||||
|
"
|
||||||
v-b-tooltip.hover
|
v-b-tooltip.hover
|
||||||
title="색상 취소"
|
title="색상 취소"
|
||||||
><i class="bi bi-droplet"></i
|
><i class="bi bi-droplet"></i
|
||||||
@@ -95,15 +97,26 @@
|
|||||||
type="color"
|
type="color"
|
||||||
class="form-control form-control-color"
|
class="form-control form-control-color"
|
||||||
:value="
|
:value="
|
||||||
colorConvert(editor.getAttributes('textStyle').backgroundColor, '#000000')
|
colorConvert(
|
||||||
|
editor.getAttributes('textStyle').backgroundColor,
|
||||||
|
'#000000'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
@input="
|
||||||
|
editor.chain().focus().setBackgroundColor($event.target.value).run()
|
||||||
"
|
"
|
||||||
@input="editor.chain().focus().setBackgroundColor($event.target.value).run()"
|
|
||||||
v-b-tooltip.hover
|
v-b-tooltip.hover
|
||||||
title="배경색"
|
title="배경색"
|
||||||
/>
|
/>
|
||||||
</b-button-group>
|
</b-button-group>
|
||||||
|
|
||||||
<b-button-group class="mx-1">
|
<b-button-group class="mx-1">
|
||||||
|
<b-button
|
||||||
|
v-b-tooltip.hover
|
||||||
|
@click="showImageModal = true"
|
||||||
|
title="이미지 추가"
|
||||||
|
><i class="bi bi-image"></i
|
||||||
|
></b-button>
|
||||||
<!-- 이미지추가 -->
|
<!-- 이미지추가 -->
|
||||||
<!-- 링크 -->
|
<!-- 링크 -->
|
||||||
<!-- 영상링크 -->
|
<!-- 영상링크 -->
|
||||||
@@ -144,8 +157,7 @@
|
|||||||
<!-- 문단정렬(왼, 가, 오, 양)(내어, 들여) -->
|
<!-- 문단정렬(왼, 가, 오, 양)(내어, 들여) -->
|
||||||
</b-button-group>
|
</b-button-group>
|
||||||
|
|
||||||
<b-button-group class="mx-1">
|
<b-button-group class="mx-1"> </b-button-group>
|
||||||
</b-button-group>
|
|
||||||
|
|
||||||
<b-button-group class="mx-1">
|
<b-button-group class="mx-1">
|
||||||
<!-- 줄간격 (1.0, 1.2, 1.4, 1.5, 1.6, 1.8, 2.0, 3.0) -->
|
<!-- 줄간격 (1.0, 1.2, 1.4, 1.5, 1.6, 1.8, 2.0, 3.0) -->
|
||||||
@@ -155,19 +167,177 @@
|
|||||||
<!-- 원본 코드 -->
|
<!-- 원본 코드 -->
|
||||||
</b-button-group>
|
</b-button-group>
|
||||||
</b-button-toolbar>
|
</b-button-toolbar>
|
||||||
|
<bubble-menu
|
||||||
|
:tippy-options="{ animation: false, maxWidth: 600 }"
|
||||||
|
:editor="editor"
|
||||||
|
v-if="editable && editor"
|
||||||
|
v-show="editor.isActive('custom-image')"
|
||||||
|
>
|
||||||
|
<b-button-toolbar>
|
||||||
|
<b-button-group class="mx-1">
|
||||||
|
<b-button
|
||||||
|
@click="editor.chain().focus().setImage({ size: 'small' }).run()"
|
||||||
|
:class="{
|
||||||
|
'is-active': editor.isActive('custom-image', {
|
||||||
|
size: 'small',
|
||||||
|
}),
|
||||||
|
f_frac: true,
|
||||||
|
}"
|
||||||
|
v-b-tooltip.hover
|
||||||
|
title="1/4 너비로 채우기"
|
||||||
|
>1/4</b-button
|
||||||
|
>
|
||||||
|
<b-button
|
||||||
|
@click="editor.chain().focus().setImage({ size: 'medium' }).run()"
|
||||||
|
:class="{
|
||||||
|
'is-active': editor.isActive('custom-image', {
|
||||||
|
size: 'medium',
|
||||||
|
}),
|
||||||
|
f_frac: true,
|
||||||
|
}"
|
||||||
|
v-b-tooltip.hover
|
||||||
|
title="1/2 너비로 채우기"
|
||||||
|
>1/2</b-button
|
||||||
|
>
|
||||||
|
<b-button
|
||||||
|
@click="editor.chain().focus().setImage({ size: 'large' }).run()"
|
||||||
|
:class="{
|
||||||
|
'is-active': editor.isActive('custom-image', {
|
||||||
|
size: 'large',
|
||||||
|
}),
|
||||||
|
f_frac: true,
|
||||||
|
}"
|
||||||
|
v-b-tooltip.hover
|
||||||
|
title="가득 채우기"
|
||||||
|
>1</b-button
|
||||||
|
>
|
||||||
|
<b-button
|
||||||
|
@click="editor.chain().focus().setImage({ size: 'original' }).run()"
|
||||||
|
:class="{
|
||||||
|
'is-active': editor.isActive('custom-image', {
|
||||||
|
size: 'original',
|
||||||
|
}),
|
||||||
|
}"
|
||||||
|
>원본</b-button
|
||||||
|
>
|
||||||
|
</b-button-group>
|
||||||
|
<b-button-group class="mx-1">
|
||||||
|
<b-button
|
||||||
|
@click="editor.chain().focus().setImage({ align: 'float-left' }).run()"
|
||||||
|
:class="{
|
||||||
|
'is-active': editor.isActive('custom-image', {
|
||||||
|
float: 'float-left',
|
||||||
|
}),
|
||||||
|
}"
|
||||||
|
v-b-tooltip.hover
|
||||||
|
title="왼쪽으로 붙이기"
|
||||||
|
><i class="bi bi-chevron-bar-left"></i
|
||||||
|
></b-button>
|
||||||
|
<b-button
|
||||||
|
@click="editor.chain().focus().setImage({ align: 'left' }).run()"
|
||||||
|
:class="{
|
||||||
|
'is-active': editor.isActive('custom-image', {
|
||||||
|
float: 'left',
|
||||||
|
}),
|
||||||
|
}"
|
||||||
|
v-b-tooltip.hover
|
||||||
|
title="왼쪽으로"
|
||||||
|
><i class="bi bi-align-start"></i
|
||||||
|
></b-button>
|
||||||
|
<b-button
|
||||||
|
@click="editor.chain().focus().setImage({ align: 'center' }).run()"
|
||||||
|
:class="{
|
||||||
|
'is-active': editor.isActive('custom-image', {
|
||||||
|
float: 'center',
|
||||||
|
}),
|
||||||
|
}"
|
||||||
|
v-b-tooltip.hover
|
||||||
|
title="가운데로"
|
||||||
|
><i class="bi bi-align-center"></i
|
||||||
|
></b-button>
|
||||||
|
<b-button
|
||||||
|
@click="editor.chain().focus().setImage({ align: 'right' }).run()"
|
||||||
|
:class="{
|
||||||
|
'is-active': editor.isActive('custom-image', {
|
||||||
|
float: 'right',
|
||||||
|
}),
|
||||||
|
}"
|
||||||
|
v-b-tooltip.hover
|
||||||
|
title="오른쪽으로 붙이기"
|
||||||
|
><i class="bi bi-align-end"></i
|
||||||
|
></b-button>
|
||||||
|
<b-button
|
||||||
|
@click="editor.chain().focus().setImage({ align: 'float-right' }).run()"
|
||||||
|
:class="{
|
||||||
|
'is-active': editor.isActive('custom-image', {
|
||||||
|
float: 'float-right',
|
||||||
|
}),
|
||||||
|
}"
|
||||||
|
v-b-tooltip.hover
|
||||||
|
title="오른쪽으로 붙이기"
|
||||||
|
><i class="bi bi-chevron-bar-right"></i
|
||||||
|
></b-button>
|
||||||
|
</b-button-group>
|
||||||
|
</b-button-toolbar>
|
||||||
|
</bubble-menu>
|
||||||
<editor-content :editor="editor" />
|
<editor-content :editor="editor" />
|
||||||
|
<b-modal
|
||||||
|
v-model="showImageModal"
|
||||||
|
title="이미지 추가"
|
||||||
|
okTitle="추가"
|
||||||
|
cancelTitle="취소"
|
||||||
|
@ok="tryAddImage"
|
||||||
|
@show="resetModal"
|
||||||
|
@hidden="resetModal"
|
||||||
|
>
|
||||||
|
<div class="bg-light text-dark">
|
||||||
|
<b-form-group
|
||||||
|
label-cols-sm="4"
|
||||||
|
label-cols-lg="3"
|
||||||
|
content-cols-sm
|
||||||
|
content-cols-lg="7"
|
||||||
|
description="업로드할 파일을 선택해주세요. (jpg, png, gif, webp)"
|
||||||
|
label="이미지 업로드"
|
||||||
|
label-align="right"
|
||||||
|
:label-for="`${uuid}_image_upload`"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
class="form-control"
|
||||||
|
type="file"
|
||||||
|
:id="`${uuid}_image_upload`"
|
||||||
|
@change="chooseImage"
|
||||||
|
accept=".jpg,.jpeg,.png,.gif,.webp"
|
||||||
|
/>
|
||||||
|
</b-form-group>
|
||||||
|
<b-form-group
|
||||||
|
label-cols-sm="4"
|
||||||
|
label-cols-lg="3"
|
||||||
|
content-cols-sm
|
||||||
|
content-cols-lg="7"
|
||||||
|
description="링크할 이미지 주소를 입력해주세요."
|
||||||
|
label="이미지 링크"
|
||||||
|
label-align="right"
|
||||||
|
:label-for="`${uuid}_image_link`"
|
||||||
|
>
|
||||||
|
<b-form-input v-model="imageLink"></b-form-input>
|
||||||
|
</b-form-group>
|
||||||
|
</div>
|
||||||
|
</b-modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
//import "@scss/common/bootstrap5.scss";
|
//import "@scss/common/bootstrap5.scss";
|
||||||
import { defineComponent } from "vue";
|
import { defineComponent } from "vue";
|
||||||
import { Editor, EditorContent } from "@tiptap/vue-3";
|
import { Editor, EditorContent, BubbleMenu } from "@tiptap/vue-3";
|
||||||
import { FontSize } from "@/tiptap-ext/FontSize";
|
import { FontSize } from "@/tiptap-ext/FontSize";
|
||||||
import StarterKit from "@tiptap/starter-kit";
|
import StarterKit from "@tiptap/starter-kit";
|
||||||
import Underline from "@tiptap/extension-underline";
|
import Underline from "@tiptap/extension-underline";
|
||||||
import TextStyle from "@tiptap/extension-text-style";
|
import TextStyle from "@tiptap/extension-text-style";
|
||||||
import TextAlign from "@tiptap/extension-text-align";
|
import TextAlign from "@tiptap/extension-text-align";
|
||||||
import Color from "@tiptap/extension-color";
|
import Color from "@tiptap/extension-color";
|
||||||
|
//import Image from "@tiptap/extension-image";
|
||||||
|
import CustomImage from "@/tiptap-ext/CustomImage";
|
||||||
|
import Link from "@tiptap/extension-link";
|
||||||
import { BackgroundColor } from "@/tiptap-ext/BackgroundColor";
|
import { BackgroundColor } from "@/tiptap-ext/BackgroundColor";
|
||||||
import {
|
import {
|
||||||
BButtonGroup,
|
BButtonGroup,
|
||||||
@@ -176,11 +346,20 @@ import {
|
|||||||
BDropdown,
|
BDropdown,
|
||||||
BDropdownItem,
|
BDropdownItem,
|
||||||
BDropdownDivider,
|
BDropdownDivider,
|
||||||
|
BModal,
|
||||||
} from "bootstrap-vue-3";
|
} from "bootstrap-vue-3";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
import { unwrap } from "@/util/unwrap";
|
||||||
|
import { getBase64FromFileObject } from "@/util/getBase64FromFileObject";
|
||||||
|
import { sammoAPI } from "@/util/sammoAPI";
|
||||||
|
import { isObject, isString } from "lodash";
|
||||||
|
import { AxiosError } from "axios";
|
||||||
|
|
||||||
const compoment = defineComponent({
|
const compoment = defineComponent({
|
||||||
components: {
|
components: {
|
||||||
EditorContent,
|
EditorContent,
|
||||||
|
BubbleMenu,
|
||||||
|
BModal,
|
||||||
BButtonGroup,
|
BButtonGroup,
|
||||||
BButtonToolbar,
|
BButtonToolbar,
|
||||||
BButton,
|
BButton,
|
||||||
@@ -189,6 +368,10 @@ const compoment = defineComponent({
|
|||||||
BDropdownDivider,
|
BDropdownDivider,
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
chooseImage(e: Event) {
|
||||||
|
const target = unwrap(e.target) as HTMLInputElement;
|
||||||
|
this.imageUploadFiles = target.files;
|
||||||
|
},
|
||||||
colorConvert(val: string | undefined, defaultVal: string) {
|
colorConvert(val: string | undefined, defaultVal: string) {
|
||||||
if (!val) {
|
if (!val) {
|
||||||
return defaultVal;
|
return defaultVal;
|
||||||
@@ -207,6 +390,54 @@ const compoment = defineComponent({
|
|||||||
}
|
}
|
||||||
return val;
|
return val;
|
||||||
},
|
},
|
||||||
|
async tryAddImage(bvModalEvt: Event) {
|
||||||
|
if (this.imageUploadFiles === null || this.imageUploadFiles.length == 0) {
|
||||||
|
this.addImageLink(bvModalEvt);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetImage = unwrap(this.imageUploadFiles.item(0));
|
||||||
|
let imageResult: {
|
||||||
|
result: true;
|
||||||
|
path: string;
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const base64Binary = await getBase64FromFileObject(targetImage);
|
||||||
|
imageResult = await sammoAPI("Misc/UploadImage", {
|
||||||
|
imageData: base64Binary,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
if (isString(e)) {
|
||||||
|
alert(e);
|
||||||
|
bvModalEvt.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isObject(e) && "response" in e) {
|
||||||
|
const axiosErr = e as AxiosError;
|
||||||
|
if (axiosErr.response?.status === 413) {
|
||||||
|
alert("허용 용량을 초과했습니다.");
|
||||||
|
bvModalEvt.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.error(e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const imagePath = imageResult.path;
|
||||||
|
this.editor.chain().focus().setImage({ src: imagePath }).run();
|
||||||
|
},
|
||||||
|
addImageLink(bvModalEvt: Event) {
|
||||||
|
if (!this.imageLink) {
|
||||||
|
alert("업로드할 이미지를 선택하거나, 이미지 주소를 입력해주세요.");
|
||||||
|
bvModalEvt.preventDefault();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this.editor.chain().focus().setImage({ src: this.imageLink }).run();
|
||||||
|
},
|
||||||
|
resetModal() {
|
||||||
|
this.imageLink = "";
|
||||||
|
this.imageUploadFiles = null;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
@@ -221,6 +452,7 @@ const compoment = defineComponent({
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
uuid: uuidv4(),
|
||||||
editor: null as unknown as InstanceType<typeof Editor>,
|
editor: null as unknown as InstanceType<typeof Editor>,
|
||||||
fontList: ["Pretendard", "맑은 고딕", "궁서", "돋움"],
|
fontList: ["Pretendard", "맑은 고딕", "궁서", "돋움"],
|
||||||
fontSize: [
|
fontSize: [
|
||||||
@@ -235,6 +467,9 @@ const compoment = defineComponent({
|
|||||||
"48px",
|
"48px",
|
||||||
"72px",
|
"72px",
|
||||||
],
|
],
|
||||||
|
imageUploadFiles: null as FileList | null,
|
||||||
|
imageLink: "",
|
||||||
|
showImageModal: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -272,6 +507,8 @@ const compoment = defineComponent({
|
|||||||
BackgroundColor.configure({
|
BackgroundColor.configure({
|
||||||
types: ["textStyle"],
|
types: ["textStyle"],
|
||||||
}),
|
}),
|
||||||
|
CustomImage,
|
||||||
|
Link,
|
||||||
],
|
],
|
||||||
editable: this.editable,
|
editable: this.editable,
|
||||||
content: this.modelValue,
|
content: this.modelValue,
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
//https://github.com/joevallender/tiptap2-image-example/blob/main/src/extensions/custom-image-3.js
|
||||||
|
import Image, { ImageOptions } from '@tiptap/extension-image'
|
||||||
|
import { mergeAttributes } from '@tiptap/core'
|
||||||
|
|
||||||
|
export interface CustomImageOptions extends ImageOptions {
|
||||||
|
sizes: string[],
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@tiptap/core' {
|
||||||
|
interface Commands<ReturnType> {
|
||||||
|
setImage: (options: { src: string, alt?: string, title?: string, size: string }) => ReturnType,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Image.extend<CustomImageOptions>({
|
||||||
|
name: 'custom-image',
|
||||||
|
|
||||||
|
addOptions() {
|
||||||
|
return {
|
||||||
|
...Image.options,
|
||||||
|
sizes: ['original', 'small', 'medium', 'large'],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addAttributes() {
|
||||||
|
return {
|
||||||
|
src: {
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
alt: {
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: 'original',
|
||||||
|
rendered: false
|
||||||
|
},
|
||||||
|
align: {
|
||||||
|
default: 'center',
|
||||||
|
rendered: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
addCommands() {
|
||||||
|
return {
|
||||||
|
// This is unchanged from the original
|
||||||
|
// Image setImage function
|
||||||
|
// However, if I extended addComands in
|
||||||
|
// the same way as addAttributes `this`
|
||||||
|
// seemed to lose context, so I've just
|
||||||
|
// copied it in here directly
|
||||||
|
setImage: (options) => ({ tr, commands }) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
if (tr.selection?.node?.type?.name == 'custom-image') {
|
||||||
|
return commands.updateAttributes('custom-image', options)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return commands.insertContent({
|
||||||
|
type: this.name,
|
||||||
|
attrs: options
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHTML({ node, HTMLAttributes }) {
|
||||||
|
// When we render the HTML, grab the
|
||||||
|
// size and add an appropriate
|
||||||
|
// corresponding class
|
||||||
|
|
||||||
|
HTMLAttributes.class = ' custom-image-' + node.attrs.size;
|
||||||
|
HTMLAttributes.class += ' custom-image-align-' + node.attrs.align;
|
||||||
|
return [
|
||||||
|
'img',
|
||||||
|
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { unwrap } from "./unwrap";
|
||||||
|
|
||||||
|
//https://stackoverflow.com/questions/36280818/how-to-convert-file-to-base64-in-javascript
|
||||||
|
export function getBase64FromFileObject(file: File): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
reader.onload = () => {
|
||||||
|
let encoded = unwrap(reader.result).toString().replace(/^data:(.*,)?/, '');
|
||||||
|
if ((encoded.length % 4) > 0) {
|
||||||
|
encoded += '='.repeat(4 - (encoded.length % 4));
|
||||||
|
}
|
||||||
|
resolve(encoded);
|
||||||
|
};
|
||||||
|
reader.onerror = error => reject(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@popperjs/core": "^2.11.0",
|
||||||
"@tiptap/extension-color": "^2.0.0-beta.9",
|
"@tiptap/extension-color": "^2.0.0-beta.9",
|
||||||
"@tiptap/extension-image": "^2.0.0-beta.24",
|
"@tiptap/extension-image": "^2.0.0-beta.24",
|
||||||
"@tiptap/extension-link": "^2.0.0-beta.33",
|
"@tiptap/extension-link": "^2.0.0-beta.33",
|
||||||
@@ -32,11 +33,13 @@
|
|||||||
"@types/linkifyjs": "^2.1.4",
|
"@types/linkifyjs": "^2.1.4",
|
||||||
"@types/quill": "^2.0.9",
|
"@types/quill": "^2.0.9",
|
||||||
"@types/select2": "^4.0.54",
|
"@types/select2": "^4.0.54",
|
||||||
|
"@types/uuid": "^8.3.3",
|
||||||
"@vueup/vue-quill": "^1.0.0-beta.7",
|
"@vueup/vue-quill": "^1.0.0-beta.7",
|
||||||
"async-validator": "^4.0.7",
|
"async-validator": "^4.0.7",
|
||||||
"axios": "^0.24.0",
|
"axios": "^0.24.0",
|
||||||
"bootstrap": "^5.1.3",
|
"bootstrap": "^5.1.3",
|
||||||
"bootstrap-vue-3": "^0.1.0",
|
"bootstrap-vue-3": "^0.1.0",
|
||||||
|
"buffer": "^6.0.3",
|
||||||
"core-js": "^3.19.3",
|
"core-js": "^3.19.3",
|
||||||
"date-fns": "^2.27.0",
|
"date-fns": "^2.27.0",
|
||||||
"downloadjs": "^1.4.7",
|
"downloadjs": "^1.4.7",
|
||||||
@@ -47,6 +50,7 @@
|
|||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"query-string": "^7.0.1",
|
"query-string": "^7.0.1",
|
||||||
"select2": "^4.0",
|
"select2": "^4.0",
|
||||||
|
"uuid": "^8.3.2",
|
||||||
"vue": "^3.2.2",
|
"vue": "^3.2.2",
|
||||||
"vue-multiselect": "^3.0.0-alpha.2",
|
"vue-multiselect": "^3.0.0-alpha.2",
|
||||||
"vue-types": "^4.1.1",
|
"vue-types": "^4.1.1",
|
||||||
|
|||||||
+1
-1
@@ -95,7 +95,7 @@ module.exports = (env, argv) => {
|
|||||||
'@': tsDir,
|
'@': tsDir,
|
||||||
'@scss': path.resolve(tsDir, '../scss'),
|
'@scss': path.resolve(tsDir, '../scss'),
|
||||||
'@util': path.resolve(tsDir, 'util'),
|
'@util': path.resolve(tsDir, 'util'),
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
mode,
|
mode,
|
||||||
entry: entryIngameVue,
|
entry: entryIngameVue,
|
||||||
|
|||||||
Reference in New Issue
Block a user