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.
@@ -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
- watch(menuOpen, (open) => {
39
- if (open && showMemo.value && !memoLoaded.value) void loadMemo()
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(memoText.value, sentiment)
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="t('authorMenu.menu')" :delay-duration="200" :content="{ side: 'top' }">
104
+ <UTooltip :text="tooltipText" :delay-duration="200" :content="{ side: 'top' }">
89
105
  <span class="inline-flex max-w-full">
90
- <UDropdownMenu v-model:open="menuOpen" :items="items" :content="{ align: 'start' }">
91
- <UButton
92
- :avatar="showAvatar ? { src: authorPhoto ?? undefined, alt: authorName ?? t('common.label.anonymous') } : undefined"
93
- :label="authorName ?? t('common.label.anonymous')"
94
- variant="ghost"
95
- color="neutral"
96
- size="xs"
97
- class="max-w-full"
98
- :ui="truncate
99
- ? { base: 'max-w-full' }
100
- : { base: 'max-w-full', label: 'whitespace-normal overflow-visible text-clip' }"
101
- />
102
- </UDropdownMenu>
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="memoText"
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="memoText || memoSentiment"
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 p-4">
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
- <UTooltip :text="t('list.likeCount', { count: post.likeCount ?? 0 })" :delay-duration="200" :content="{ side: 'top' }">
105
- <span class="inline-flex shrink-0 items-center gap-1 tabular-nums">
106
- <UIcon name="i-lucide-heart" class="size-3.5" />{{ post.likeCount ?? 0 }}
107
- </span>
108
- </UTooltip>
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 { T as e, Z as t, a as n, c as r, h as i, n as a } from "./commentImages-xb5Y4ySe.js";
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 = () => e(), h = o(), { t: g, locale: _ } = r();
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(e, r, a) {
68
- let o = u(h), s = `${t(i(), r)}/attachments/${Date.now()}-${n(e.name)}`, c = d(o, s), f = p(c, e, {
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: e.type || "application/octet-stream",
71
- contentDisposition: `attachment; filename*=UTF-8''${encodeURIComponent(e.name)}`
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((t, n) => {
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
- t({
80
- name: e.name,
79
+ e({
80
+ name: t.name,
81
81
  url: n,
82
82
  path: s,
83
- size: e.size,
84
- type: e.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(e, r, o, s) {
96
- if (!a(e)) throw Error(g("commentForm.imageOnly"));
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(e, _.value);
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 || e.type || "image/jpeg").toLowerCase(), v = h === "image/png" ? "png" : h === "image/gif" ? "gif" : h === "image/webp" ? "webp" : "jpg", S = n(e.name.replace(/\.[^.]+$/, "") || "image", 40) || "image", C = `${t(i(), r)}/comment-images/${o}/${Date.now()}-${S}.${v}`, w = d(f, C);
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: e.name || `image.${v}`,
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(e, r) {
134
- if (!e.type.startsWith("image/")) throw Error(g("storage.imageOnly"));
135
- let a = await b(e, _.value), o = u(h), s = (a.compressed ? "jpg" : e.name.split(".").pop() || e.type.split("/")[1] || "png").replace(/[^a-zA-Z0-9]/g, "").slice(0, 8) || "png", c = n(e.name.replace(/\.[^.]+$/, "") || "image", 40), p = `${Date.now()}-${c || "image"}`, y = r || `new-${Date.now()}`, x = t(i(), y), S = `${x}/images/${p}.${s}`, C = d(o, S);
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memi-board",
3
- "version": "0.64.0",
3
+ "version": "0.65.0",
4
4
  "type": "module",
5
5
  "description": "Board (forum) module for Nuxt 4 + Nuxt UI + VueFire/Firebase",
6
6
  "license": "MIT",