memi-board 0.64.0 → 0.65.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{commentImages-xb5Y4ySe.js → commentImages-DHIuwvex.js} +158 -150
- package/dist/commentImages-DHIuwvex.js.map +1 -0
- package/dist/composables/useMemiBoardAuthorMemo.d.ts +18 -0
- package/dist/{en-I7auZHra.js → en-yS55nNno.js} +8 -3
- package/dist/en-yS55nNno.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1395 -1316
- package/dist/index.js.map +1 -1
- package/dist/runtime/components/AuthorMemoList.vue +83 -0
- package/dist/runtime/components/AuthorMemoListItem.vue +47 -0
- package/dist/runtime/components/AuthorMenu.vue +54 -21
- package/dist/runtime/components/CategorySettings.vue +2 -0
- package/dist/runtime/components/ListCard.vue +18 -11
- package/dist/runtime/components/OrphanCleanup.vue +2 -0
- package/dist/runtime/components/Profile.vue +14 -0
- package/dist/runtime/components/ReportList.vue +2 -0
- package/dist/runtime/components/Settings.vue +2 -0
- package/dist/runtime/components/SettingsVersion.vue +10 -0
- package/dist/runtime/components/UsersSettings.vue +2 -0
- package/dist/storage.js +19 -19
- package/dist/utils/boardPaths.d.ts +2 -0
- package/package.json +1 -1
- package/dist/commentImages-xb5Y4ySe.js.map +0 -1
- package/dist/en-I7auZHra.js.map +0 -1
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { onMounted, ref } from 'vue'
|
|
3
|
+
import { AUTHOR_MEMO_MAX_COUNT, useMemiBoardI18n, useMemiBoardMyAuthorMemos } from 'memi-board/runtime'
|
|
4
|
+
import MemiBoardAuthorMemoListItem from './AuthorMemoListItem.vue'
|
|
5
|
+
|
|
6
|
+
defineProps<{
|
|
7
|
+
authorProfileTo?: (authorUid: string) => string | undefined
|
|
8
|
+
authorPostsTo?: (authorUid: string) => string | undefined
|
|
9
|
+
authorCommentsTo?: (authorUid: string) => string | undefined
|
|
10
|
+
}>()
|
|
11
|
+
|
|
12
|
+
const { t } = useMemiBoardI18n()
|
|
13
|
+
const { memos, pending, error, load } = useMemiBoardMyAuthorMemos()
|
|
14
|
+
|
|
15
|
+
// 기본은 접어둔다. 개수 배지 때문에 마운트 시 불러온다(게시판 닷과 같은 쿼리 1회·캐시 공유).
|
|
16
|
+
const open = ref(false)
|
|
17
|
+
onMounted(() => { void load() })
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<template>
|
|
21
|
+
<UCollapsible v-model:open="open" class="flex flex-col gap-2">
|
|
22
|
+
<UButton
|
|
23
|
+
icon="i-lucide-notebook-pen"
|
|
24
|
+
color="neutral"
|
|
25
|
+
variant="ghost"
|
|
26
|
+
block
|
|
27
|
+
class="justify-start font-semibold text-highlighted"
|
|
28
|
+
:trailing-icon="open ? 'i-lucide-chevron-up' : 'i-lucide-chevron-down'"
|
|
29
|
+
:ui="{ trailingIcon: 'ms-auto' }"
|
|
30
|
+
>
|
|
31
|
+
<template #default>
|
|
32
|
+
{{ t('authorMenu.myMemosTitle') }}
|
|
33
|
+
<UBadge
|
|
34
|
+
v-if="memos.length"
|
|
35
|
+
:label="`${memos.length} / ${AUTHOR_MEMO_MAX_COUNT}`"
|
|
36
|
+
:color="memos.length >= AUTHOR_MEMO_MAX_COUNT ? 'warning' : 'neutral'"
|
|
37
|
+
variant="subtle"
|
|
38
|
+
size="sm"
|
|
39
|
+
/>
|
|
40
|
+
</template>
|
|
41
|
+
</UButton>
|
|
42
|
+
|
|
43
|
+
<template #content>
|
|
44
|
+
<div class="flex flex-col gap-2">
|
|
45
|
+
<UAlert
|
|
46
|
+
v-if="error"
|
|
47
|
+
color="error"
|
|
48
|
+
variant="subtle"
|
|
49
|
+
:description="error"
|
|
50
|
+
/>
|
|
51
|
+
|
|
52
|
+
<div
|
|
53
|
+
v-else-if="pending && !memos.length"
|
|
54
|
+
class="flex flex-col gap-2"
|
|
55
|
+
>
|
|
56
|
+
<USkeleton class="h-10 w-full" />
|
|
57
|
+
<USkeleton class="h-10 w-full" />
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<p
|
|
61
|
+
v-else-if="!memos.length"
|
|
62
|
+
class="text-sm text-muted"
|
|
63
|
+
>
|
|
64
|
+
{{ t('authorMenu.myMemosEmpty') }}
|
|
65
|
+
</p>
|
|
66
|
+
|
|
67
|
+
<div
|
|
68
|
+
v-else
|
|
69
|
+
class="divide-y divide-default"
|
|
70
|
+
>
|
|
71
|
+
<MemiBoardAuthorMemoListItem
|
|
72
|
+
v-for="memo in memos"
|
|
73
|
+
:key="memo.targetUid"
|
|
74
|
+
:memo="memo"
|
|
75
|
+
:author-profile-to="authorProfileTo"
|
|
76
|
+
:author-posts-to="authorPostsTo"
|
|
77
|
+
:author-comments-to="authorCommentsTo"
|
|
78
|
+
/>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</template>
|
|
82
|
+
</UCollapsible>
|
|
83
|
+
</template>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import { useMemiBoardI18n, useMemiBoardUserProfile } from 'memi-board/runtime'
|
|
4
|
+
import type { MyAuthorMemo } from 'memi-board/runtime'
|
|
5
|
+
import MemiBoardAuthorMenu from './AuthorMenu.vue'
|
|
6
|
+
|
|
7
|
+
const props = defineProps<{
|
|
8
|
+
memo: MyAuthorMemo
|
|
9
|
+
authorProfileTo?: (authorUid: string) => string | undefined
|
|
10
|
+
authorPostsTo?: (authorUid: string) => string | undefined
|
|
11
|
+
authorCommentsTo?: (authorUid: string) => string | undefined
|
|
12
|
+
}>()
|
|
13
|
+
|
|
14
|
+
const { t } = useMemiBoardI18n()
|
|
15
|
+
const { profile } = useMemiBoardUserProfile(() => props.memo.targetUid)
|
|
16
|
+
const name = computed(() => profile.value?.displayName ?? t('common.label.anonymous'))
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<template>
|
|
20
|
+
<div class="flex items-start gap-3 py-3">
|
|
21
|
+
<div class="flex min-w-0 flex-1 flex-col gap-1">
|
|
22
|
+
<MemiBoardAuthorMenu
|
|
23
|
+
:author-uid="memo.targetUid"
|
|
24
|
+
:author-name="name"
|
|
25
|
+
:author-photo="profile?.photoURL"
|
|
26
|
+
:author-profile-to="authorProfileTo"
|
|
27
|
+
:author-posts-to="authorPostsTo"
|
|
28
|
+
:author-comments-to="authorCommentsTo"
|
|
29
|
+
class="-ms-2 self-start"
|
|
30
|
+
/>
|
|
31
|
+
<p
|
|
32
|
+
v-if="memo.text"
|
|
33
|
+
class="break-words whitespace-pre-line text-sm text-toned"
|
|
34
|
+
>
|
|
35
|
+
{{ memo.text }}
|
|
36
|
+
</p>
|
|
37
|
+
</div>
|
|
38
|
+
<UBadge
|
|
39
|
+
v-if="memo.sentiment"
|
|
40
|
+
:label="memo.sentiment === 'good' ? t('authorMenu.good') : t('authorMenu.bad')"
|
|
41
|
+
:color="memo.sentiment === 'good' ? 'primary' : 'error'"
|
|
42
|
+
variant="subtle"
|
|
43
|
+
size="sm"
|
|
44
|
+
class="mt-1 shrink-0"
|
|
45
|
+
/>
|
|
46
|
+
</div>
|
|
47
|
+
</template>
|
|
@@ -30,15 +30,29 @@ const authorCommentsLink = computed(() => props.authorUid ? props.authorComments
|
|
|
30
30
|
const { user } = useMemiBoardAuth()
|
|
31
31
|
const showMemo = computed(() => !!props.authorUid && !!user.value?.uid && user.value.uid !== props.authorUid)
|
|
32
32
|
const { text: memoText, sentiment: memoSentiment, loaded: memoLoaded, saving: memoSaving, error: memoError, load: loadMemo, save: saveMemo, remove: removeMemo } = useMemiBoardAuthorMemo(() => props.authorUid)
|
|
33
|
-
const menuOpen = ref(false)
|
|
34
33
|
const memoOpen = ref(false)
|
|
35
34
|
const memoPendingSentiment = ref<'good' | 'bad' | null>(null)
|
|
35
|
+
// 모달에서 고치는 중인 텍스트 — 저장 전에는 이름 옆 표시·툴팁에 새지 않게 따로 둔다.
|
|
36
|
+
const memoDraft = ref('')
|
|
36
37
|
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
// 이름 옆 아이콘·툴팁으로 메모를 바로 보여줘야 해서 마운트 시 불러온다.
|
|
39
|
+
// 뷰어의 메모 전체를 쿼리 한 번으로 읽고 모든 인스턴스가 공유하므로 작성자 수만큼 읽지 않는다.
|
|
40
|
+
watch(showMemo, (show) => {
|
|
41
|
+
if (show && !memoLoaded.value) void loadMemo()
|
|
42
|
+
}, { immediate: true })
|
|
43
|
+
|
|
44
|
+
watch(memoOpen, (open) => {
|
|
45
|
+
if (open) memoDraft.value = memoText.value
|
|
40
46
|
})
|
|
41
47
|
|
|
48
|
+
const hasMemo = computed(() => showMemo.value && (!!memoText.value || !!memoSentiment.value))
|
|
49
|
+
// 메모가 있으면 아바타(없으면 버튼) 모서리에 평가 색 닷을 찍는다 — 좋음 primary, 나쁨 error, 평가 없음 neutral.
|
|
50
|
+
const memoChipColor = computed(() => memoSentiment.value === 'good' ? 'primary' as const : memoSentiment.value === 'bad' ? 'error' as const : 'neutral' as const)
|
|
51
|
+
const avatar = computed(() => props.showAvatar
|
|
52
|
+
? { src: props.authorPhoto ?? undefined, alt: props.authorName ?? t('common.label.anonymous'), chip: hasMemo.value ? { color: memoChipColor.value } : undefined }
|
|
53
|
+
: undefined)
|
|
54
|
+
const tooltipText = computed(() => hasMemo.value && memoText.value ? memoText.value : t('authorMenu.menu'))
|
|
55
|
+
|
|
42
56
|
const items = computed(() => [
|
|
43
57
|
[
|
|
44
58
|
{ label: t('authorMenu.viewProfile'), icon: 'i-lucide-user-round', to: authorProfileLink.value, disabled: !authorProfileLink.value },
|
|
@@ -49,6 +63,8 @@ const items = computed(() => [
|
|
|
49
63
|
? [[{
|
|
50
64
|
label: t('authorMenu.memoTitle'),
|
|
51
65
|
icon: 'i-lucide-notebook-pen',
|
|
66
|
+
slot: 'memo' as const,
|
|
67
|
+
description: memoText.value || undefined,
|
|
52
68
|
color: memoSentiment.value === 'good' ? 'primary' as const : memoSentiment.value === 'bad' ? 'error' as const : undefined,
|
|
53
69
|
onSelect: () => { memoOpen.value = true },
|
|
54
70
|
}]]
|
|
@@ -58,7 +74,7 @@ const items = computed(() => [
|
|
|
58
74
|
async function onSaveMemo(sentiment: 'good' | 'bad') {
|
|
59
75
|
memoPendingSentiment.value = sentiment
|
|
60
76
|
try {
|
|
61
|
-
await saveMemo(
|
|
77
|
+
await saveMemo(memoDraft.value, sentiment)
|
|
62
78
|
memoOpen.value = false
|
|
63
79
|
}
|
|
64
80
|
catch {
|
|
@@ -85,21 +101,38 @@ async function onDeleteMemo() {
|
|
|
85
101
|
class="flex max-w-full items-center"
|
|
86
102
|
:class="truncate ? 'w-full min-w-0 justify-end' : 'inline-flex'"
|
|
87
103
|
>
|
|
88
|
-
<UTooltip :text="
|
|
104
|
+
<UTooltip :text="tooltipText" :delay-duration="200" :content="{ side: 'top' }">
|
|
89
105
|
<span class="inline-flex max-w-full">
|
|
90
|
-
<
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
106
|
+
<UChip
|
|
107
|
+
:show="hasMemo && !showAvatar"
|
|
108
|
+
:color="memoChipColor"
|
|
109
|
+
class="max-w-full"
|
|
110
|
+
>
|
|
111
|
+
<UDropdownMenu :items="items" :content="{ align: 'start' }">
|
|
112
|
+
<UButton
|
|
113
|
+
:avatar="avatar"
|
|
114
|
+
:label="authorName ?? t('common.label.anonymous')"
|
|
115
|
+
variant="ghost"
|
|
116
|
+
color="neutral"
|
|
117
|
+
size="xs"
|
|
118
|
+
class="max-w-full"
|
|
119
|
+
:aria-label="hasMemo ? `${authorName ?? t('common.label.anonymous')} — ${t('authorMenu.memoAriaLabel')}` : undefined"
|
|
120
|
+
:ui="truncate
|
|
121
|
+
? { base: 'max-w-full' }
|
|
122
|
+
: { base: 'max-w-full', label: 'whitespace-normal overflow-visible text-clip' }"
|
|
123
|
+
/>
|
|
124
|
+
<template #memo-trailing>
|
|
125
|
+
<UBadge
|
|
126
|
+
v-if="memoSentiment"
|
|
127
|
+
:label="memoSentiment === 'good' ? t('authorMenu.good') : t('authorMenu.bad')"
|
|
128
|
+
:color="memoSentiment === 'good' ? 'primary' : 'error'"
|
|
129
|
+
variant="subtle"
|
|
130
|
+
size="sm"
|
|
131
|
+
class="ms-auto shrink-0"
|
|
132
|
+
/>
|
|
133
|
+
</template>
|
|
134
|
+
</UDropdownMenu>
|
|
135
|
+
</UChip>
|
|
103
136
|
</span>
|
|
104
137
|
</UTooltip>
|
|
105
138
|
|
|
@@ -111,7 +144,7 @@ async function onDeleteMemo() {
|
|
|
111
144
|
>
|
|
112
145
|
<template #body>
|
|
113
146
|
<UTextarea
|
|
114
|
-
v-model="
|
|
147
|
+
v-model="memoDraft"
|
|
115
148
|
:rows="3"
|
|
116
149
|
:maxlength="AUTHOR_MEMO_MAX_LENGTH"
|
|
117
150
|
:placeholder="t('authorMenu.memoPlaceholder')"
|
|
@@ -127,7 +160,7 @@ async function onDeleteMemo() {
|
|
|
127
160
|
</template>
|
|
128
161
|
<template #footer>
|
|
129
162
|
<UButton
|
|
130
|
-
v-if="
|
|
163
|
+
v-if="hasMemo"
|
|
131
164
|
:label="t('common.action.delete')"
|
|
132
165
|
color="neutral"
|
|
133
166
|
variant="ghost"
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import MemiBoardSurface from './BoardSurface.vue'
|
|
3
|
+
import MemiBoardSettingsVersion from './SettingsVersion.vue'
|
|
3
4
|
import { useMemiBoardStorage } from 'memi-board/storage'
|
|
4
5
|
import { computed, onMounted, ref, watch } from 'vue'
|
|
5
6
|
import { boardCopyIsTranslated, boardTranslationLocales, MEMI_BOARD_LOCALES, memiBoardEnabledLocales, useMemiBoardAuth, useMemiBoardPostTranslations, useMemiBoardI18n, useMemiBoardSettings, useMemiBoardUsers } from 'memi-board/runtime'
|
|
@@ -412,5 +413,6 @@ async function save() {
|
|
|
412
413
|
<UAlert v-if="error" color="error" variant="subtle" :description="error" />
|
|
413
414
|
<UAlert v-if="saved" color="success" variant="subtle" :description="isNew ? t('settings.created') : t('settings.savedSettings')" />
|
|
414
415
|
</div>
|
|
416
|
+
<MemiBoardSettingsVersion />
|
|
415
417
|
</MemiBoardSurface>
|
|
416
418
|
</template>
|
|
@@ -56,15 +56,15 @@ function image(post: PostModel): string | undefined {
|
|
|
56
56
|
class="memi-board-post-link flex flex-1 flex-col text-left text-highlighted"
|
|
57
57
|
@click="!postTo(post) && emit('select', post)"
|
|
58
58
|
>
|
|
59
|
+
<h3 class="line-clamp-2 break-words px-4 pb-3 pt-4 text-base font-bold leading-snug text-inherit">
|
|
60
|
+
<UBadge v-if="post.isPublished === false" :label="t('common.label.draft')" color="warning" variant="subtle" size="sm" class="me-1 align-middle" />
|
|
61
|
+
<UIcon v-if="post.pinned" name="i-lucide-pin" class="me-1 align-[-0.125em] text-primary" :aria-label="t('list.notice')" />
|
|
62
|
+
{{ titleOf(post) }}
|
|
63
|
+
</h3>
|
|
59
64
|
<div v-if="image(post)" class="aspect-video w-full overflow-hidden bg-elevated">
|
|
60
65
|
<img :src="image(post)" :alt="titleOf(post)" class="size-full object-cover transition-transform duration-300 hover:scale-105">
|
|
61
66
|
</div>
|
|
62
|
-
<div class="flex flex-1 flex-col gap-2
|
|
63
|
-
<h3 class="line-clamp-2 break-words text-base font-bold leading-snug text-inherit">
|
|
64
|
-
<UBadge v-if="post.isPublished === false" :label="t('common.label.draft')" color="warning" variant="subtle" size="sm" class="me-1 align-middle" />
|
|
65
|
-
<UIcon v-if="post.pinned" name="i-lucide-pin" class="me-1 align-[-0.125em] text-primary" :aria-label="t('list.notice')" />
|
|
66
|
-
{{ titleOf(post) }}
|
|
67
|
-
</h3>
|
|
67
|
+
<div class="flex flex-1 flex-col gap-2 px-4 pb-4" :class="image(post) ? 'pt-3' : 'pt-0'">
|
|
68
68
|
<p v-if="summaryOf(post)" class="line-clamp-3 break-words whitespace-pre-line text-sm leading-relaxed text-muted">
|
|
69
69
|
{{ summaryOf(post) }}
|
|
70
70
|
</p>
|
|
@@ -101,11 +101,18 @@ function image(post: PostModel): string | undefined {
|
|
|
101
101
|
truncate
|
|
102
102
|
class="min-w-0"
|
|
103
103
|
/>
|
|
104
|
-
<
|
|
105
|
-
<
|
|
106
|
-
<
|
|
107
|
-
|
|
108
|
-
|
|
104
|
+
<div class="flex shrink-0 items-center gap-3">
|
|
105
|
+
<UTooltip :text="t('common.label.viewCount', { count: post.viewCount ?? 0 })" :delay-duration="200" :content="{ side: 'top' }">
|
|
106
|
+
<span class="inline-flex items-center gap-1 tabular-nums">
|
|
107
|
+
<UIcon name="i-lucide-eye" class="size-3.5" />{{ post.viewCount ?? 0 }}
|
|
108
|
+
</span>
|
|
109
|
+
</UTooltip>
|
|
110
|
+
<UTooltip :text="t('list.likeCount', { count: post.likeCount ?? 0 })" :delay-duration="200" :content="{ side: 'top' }">
|
|
111
|
+
<span class="inline-flex items-center gap-1 tabular-nums">
|
|
112
|
+
<UIcon name="i-lucide-heart" class="size-3.5" />{{ post.likeCount ?? 0 }}
|
|
113
|
+
</span>
|
|
114
|
+
</UTooltip>
|
|
115
|
+
</div>
|
|
109
116
|
</div>
|
|
110
117
|
</template>
|
|
111
118
|
</UCard>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import MemiBoardSurface from './BoardSurface.vue'
|
|
3
|
+
import MemiBoardSettingsVersion from './SettingsVersion.vue'
|
|
3
4
|
import { computed, ref } from 'vue'
|
|
4
5
|
import { useMemiBoardCleanup, useMemiBoardI18n } from 'memi-board/runtime'
|
|
5
6
|
|
|
@@ -57,5 +58,6 @@ async function deleteSelected() {
|
|
|
57
58
|
</template>
|
|
58
59
|
<p v-if="result" role="status" class="text-sm">{{ result }}</p>
|
|
59
60
|
</section>
|
|
61
|
+
<MemiBoardSettingsVersion />
|
|
60
62
|
</MemiBoardSurface>
|
|
61
63
|
</template>
|
|
@@ -3,11 +3,16 @@ import MemiBoardSurface from './BoardSurface.vue'
|
|
|
3
3
|
import { computed, ref } from 'vue'
|
|
4
4
|
import { useMemiBoardAuth, useMemiBoardI18n, useMemiBoardUserProfile, useMemiBoardUserStats, WRITING_ASSISTANT_DAILY_LIMIT } from 'memi-board/runtime'
|
|
5
5
|
import MemiBoardProfileEditModal from './ProfileEditModal.vue'
|
|
6
|
+
import MemiBoardAuthorMemoList from './AuthorMemoList.vue'
|
|
6
7
|
|
|
7
8
|
const props = defineProps<{
|
|
8
9
|
uid: string
|
|
9
10
|
/** 본인 프로필의 수정 버튼 대신 표시할 호스트 계정 페이지 주소. */
|
|
10
11
|
accountTo?: string
|
|
12
|
+
/** 본인 프로필의 "내가 남긴 메모" 목록에서 작성자 메뉴 링크로 쓴다. */
|
|
13
|
+
authorProfileTo?: (authorUid: string) => string | undefined
|
|
14
|
+
authorPostsTo?: (authorUid: string) => string | undefined
|
|
15
|
+
authorCommentsTo?: (authorUid: string) => string | undefined
|
|
11
16
|
}>()
|
|
12
17
|
|
|
13
18
|
const { profile, profilePending } = useMemiBoardUserProfile(() => props.uid)
|
|
@@ -134,6 +139,15 @@ const aiUsage = computed(() => {
|
|
|
134
139
|
>
|
|
135
140
|
{{ profile.bio }}
|
|
136
141
|
</p>
|
|
142
|
+
|
|
143
|
+
<template v-if="isOwnProfile">
|
|
144
|
+
<USeparator class="mt-2" />
|
|
145
|
+
<MemiBoardAuthorMemoList
|
|
146
|
+
:author-profile-to="authorProfileTo"
|
|
147
|
+
:author-posts-to="authorPostsTo"
|
|
148
|
+
:author-comments-to="authorCommentsTo"
|
|
149
|
+
/>
|
|
150
|
+
</template>
|
|
137
151
|
</div>
|
|
138
152
|
|
|
139
153
|
<MemiBoardProfileEditModal
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import MemiBoardSurface from './BoardSurface.vue'
|
|
3
|
+
import MemiBoardSettingsVersion from './SettingsVersion.vue'
|
|
3
4
|
import { computed, ref, resolveComponent, watch } from 'vue'
|
|
4
5
|
import {
|
|
5
6
|
imeSafeSubmitClick,
|
|
@@ -128,5 +129,6 @@ async function handle() {
|
|
|
128
129
|
</form>
|
|
129
130
|
</template>
|
|
130
131
|
</UModal>
|
|
132
|
+
<MemiBoardSettingsVersion />
|
|
131
133
|
</MemiBoardSurface>
|
|
132
134
|
</template>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import MemiBoardSurface from './BoardSurface.vue'
|
|
3
|
+
import MemiBoardSettingsVersion from './SettingsVersion.vue'
|
|
3
4
|
import { useMemiBoardStorage } from 'memi-board/storage'
|
|
4
5
|
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
|
5
6
|
import { BOARD_NAME_MAX, boardCopyIsTranslated, boardTranslationLocales, MEMI_BOARD_LOCALES, memiBoardEnabledLocales, slugify, useMemiBoardAppSettings, useMemiBoardAuth, useMemiBoardI18n, useMemiBoardSettings, useMemiBoardUsers } from 'memi-board/runtime'
|
|
@@ -979,5 +980,6 @@ async function runDeleteAll() {
|
|
|
979
980
|
<UAlert v-if="deleteDone" color="success" variant="subtle" :description="t('settings.danger.done')" />
|
|
980
981
|
</section>
|
|
981
982
|
</div>
|
|
983
|
+
<MemiBoardSettingsVersion />
|
|
982
984
|
</MemiBoardSurface>
|
|
983
985
|
</template>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { versionHistory } from 'memi-board/runtime'
|
|
3
|
+
|
|
4
|
+
/** 설정·관리 화면 맨 아래에 붙는 패키지 버전. 문의할 때 어떤 버전인지 바로 알 수 있게 한다. */
|
|
5
|
+
const version = versionHistory[0]?.version
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<template>
|
|
9
|
+
<p v-if="version" class="mt-8 text-center font-mono text-xs text-dimmed">memi-board™ v{{ version }}</p>
|
|
10
|
+
</template>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import MemiBoardSurface from './BoardSurface.vue'
|
|
3
|
+
import MemiBoardSettingsVersion from './SettingsVersion.vue'
|
|
3
4
|
import UserManagementEmail from './UserManagementEmail.vue'
|
|
4
5
|
import UserRoleGuide from './UserRoleGuide.vue'
|
|
5
6
|
import UserListControls from './UserListControls.vue'
|
|
@@ -115,5 +116,6 @@ async function confirmRoleChange() {
|
|
|
115
116
|
</div>
|
|
116
117
|
</template>
|
|
117
118
|
</UModal>
|
|
119
|
+
<MemiBoardSettingsVersion />
|
|
118
120
|
</MemiBoardSurface>
|
|
119
121
|
</template>
|
package/dist/storage.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Q as e, T as t, a as n, c as r, h as i, n as a } from "./commentImages-DHIuwvex.js";
|
|
2
2
|
import { useFirebaseApp as o } from "vuefire";
|
|
3
3
|
import { getAuth as s } from "firebase/auth";
|
|
4
4
|
import { deleteObject as c, getDownloadURL as l, getStorage as u, ref as d, uploadBytes as f, uploadBytesResumable as p } from "firebase/storage";
|
|
@@ -58,30 +58,30 @@ async function b(e, t) {
|
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
function x() {
|
|
61
|
-
let i = () =>
|
|
61
|
+
let i = () => t(), h = o(), { t: g, locale: _ } = r();
|
|
62
62
|
function v() {
|
|
63
63
|
let e = s(h).currentUser?.uid;
|
|
64
64
|
if (!e) throw Error("Sign in before uploading a file.");
|
|
65
65
|
return { ownerUid: e };
|
|
66
66
|
}
|
|
67
|
-
function y(
|
|
68
|
-
let o = u(h), s = `${
|
|
67
|
+
function y(t, r, a) {
|
|
68
|
+
let o = u(h), s = `${e(i(), r)}/attachments/${Date.now()}-${n(t.name)}`, c = d(o, s), f = p(c, t, {
|
|
69
69
|
customMetadata: v(),
|
|
70
|
-
contentType:
|
|
71
|
-
contentDisposition: `attachment; filename*=UTF-8''${encodeURIComponent(
|
|
70
|
+
contentType: t.type || "application/octet-stream",
|
|
71
|
+
contentDisposition: `attachment; filename*=UTF-8''${encodeURIComponent(t.name)}`
|
|
72
72
|
});
|
|
73
73
|
return {
|
|
74
|
-
promise: new Promise((
|
|
74
|
+
promise: new Promise((e, n) => {
|
|
75
75
|
f.on("state_changed", (e) => {
|
|
76
76
|
a && e.totalBytes > 0 && a(e.bytesTransferred / e.totalBytes);
|
|
77
77
|
}, n, async () => {
|
|
78
78
|
let n = await l(c);
|
|
79
|
-
|
|
80
|
-
name:
|
|
79
|
+
e({
|
|
80
|
+
name: t.name,
|
|
81
81
|
url: n,
|
|
82
82
|
path: s,
|
|
83
|
-
size:
|
|
84
|
-
type:
|
|
83
|
+
size: t.size,
|
|
84
|
+
type: t.type
|
|
85
85
|
});
|
|
86
86
|
});
|
|
87
87
|
}),
|
|
@@ -92,13 +92,13 @@ function x() {
|
|
|
92
92
|
let t = u(h);
|
|
93
93
|
await c(d(t, e.path)).catch(() => {});
|
|
94
94
|
}
|
|
95
|
-
function S(
|
|
96
|
-
if (!a(
|
|
95
|
+
function S(t, r, o, s) {
|
|
96
|
+
if (!a(t)) throw Error(g("commentForm.imageOnly"));
|
|
97
97
|
if (!/^[A-Za-z0-9]{10,128}$/.test(r) || !/^[A-Za-z0-9]{10,128}$/.test(o)) throw Error(g("commentForm.uploadFailed"));
|
|
98
98
|
let c = v(), f = u(h), y, x = !1;
|
|
99
99
|
return {
|
|
100
100
|
promise: (async () => {
|
|
101
|
-
let a = await b(
|
|
101
|
+
let a = await b(t, _.value);
|
|
102
102
|
if (x) throw Error(g("commentForm.uploadFailed"));
|
|
103
103
|
let u = a.blob;
|
|
104
104
|
if (u.size >= 5242880 && (u = await m(u, {
|
|
@@ -106,7 +106,7 @@ function x() {
|
|
|
106
106
|
quality: .75
|
|
107
107
|
}, _.value)), x) throw Error(g("commentForm.uploadFailed"));
|
|
108
108
|
if (u.size >= 5242880) throw Error(g("storage.tooLargeAfterCompress"));
|
|
109
|
-
let h = (u.type ||
|
|
109
|
+
let h = (u.type || t.type || "image/jpeg").toLowerCase(), v = h === "image/png" ? "png" : h === "image/gif" ? "gif" : h === "image/webp" ? "webp" : "jpg", S = n(t.name.replace(/\.[^.]+$/, "") || "image", 40) || "image", C = `${e(i(), r)}/comment-images/${o}/${Date.now()}-${S}.${v}`, w = d(f, C);
|
|
110
110
|
if (y = p(w, u, {
|
|
111
111
|
customMetadata: c,
|
|
112
112
|
contentType: h
|
|
@@ -118,7 +118,7 @@ function x() {
|
|
|
118
118
|
});
|
|
119
119
|
let T = await l(w);
|
|
120
120
|
return {
|
|
121
|
-
name:
|
|
121
|
+
name: t.name || `image.${v}`,
|
|
122
122
|
url: T,
|
|
123
123
|
path: C,
|
|
124
124
|
size: u.size,
|
|
@@ -130,9 +130,9 @@ function x() {
|
|
|
130
130
|
}
|
|
131
131
|
};
|
|
132
132
|
}
|
|
133
|
-
async function C(
|
|
134
|
-
if (!
|
|
135
|
-
let a = await b(
|
|
133
|
+
async function C(t, r) {
|
|
134
|
+
if (!t.type.startsWith("image/")) throw Error(g("storage.imageOnly"));
|
|
135
|
+
let a = await b(t, _.value), o = u(h), s = (a.compressed ? "jpg" : t.name.split(".").pop() || t.type.split("/")[1] || "png").replace(/[^a-zA-Z0-9]/g, "").slice(0, 8) || "png", c = n(t.name.replace(/\.[^.]+$/, "") || "image", 40), p = `${Date.now()}-${c || "image"}`, y = r || `new-${Date.now()}`, x = e(i(), y), S = `${x}/images/${p}.${s}`, C = d(o, S);
|
|
136
136
|
await f(C, a.blob, {
|
|
137
137
|
customMetadata: v(),
|
|
138
138
|
contentType: a.blob.type || "image/jpeg"
|
|
@@ -54,6 +54,8 @@ export declare function boardUserDoc(db: Firestore, cfg: BoardPathConfig, uid: s
|
|
|
54
54
|
export declare function boardUserPrivateDoc(db: Firestore, cfg: BoardPathConfig, uid: string): DocumentReference;
|
|
55
55
|
/** 본인만 보는 작성자 메모 — 문서 ID 자체가 targetUid라 대상당 메모가 하나로 고정된다. */
|
|
56
56
|
export declare function authorMemoDoc(db: Firestore, cfg: BoardPathConfig, viewerUid: string, targetUid: string): DocumentReference;
|
|
57
|
+
/** 내가 남긴 작성자 메모 전체 — 목록 화면에서 한 번의 쿼리로 모두 불러올 때 쓴다. */
|
|
58
|
+
export declare function authorMemosCollection(db: Firestore, cfg: BoardPathConfig, viewerUid: string): CollectionReference;
|
|
57
59
|
/** postId 가 전역 고유이므로 boardId 프리픽스 불필요 */
|
|
58
60
|
export declare function postStorageFolder(cfg: BoardPathConfig, postId: string): string;
|
|
59
61
|
export declare function boardSsrKey(cfg: BoardPathConfig, suffix: string): string;
|