memi-board 0.46.2 → 0.47.1
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/README.md +141 -139
- package/dist/config.d.ts +14 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1080 -1018
- package/dist/index.js.map +1 -1
- package/dist/module.mjs +7 -0
- package/dist/module.mjs.map +1 -1
- package/dist/runtime/components/CategorySettings.vue +8 -7
- package/dist/runtime/components/Editor.vue +5 -5
- package/dist/runtime/components/Settings.vue +8 -7
- package/dist/{slugify-B05Qykf_.js → slugify-DzXnx16m.js} +109 -92
- package/dist/slugify-DzXnx16m.js.map +1 -0
- package/dist/storage.js +1 -1
- package/dist/types.d.ts +3 -1
- package/docs/firebase-setup.md +17 -13
- package/docs/firestore.rules.example +39 -1
- package/docs/host-integration.md +355 -0
- package/docs/internationalization.md +180 -0
- package/docs/roles-migration.md +4 -0
- package/package.json +3 -2
- package/dist/slugify-B05Qykf_.js.map +0 -1
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
# 다른 Nuxt 사이트에 memi-board 붙이기
|
|
2
|
+
|
|
3
|
+
[README](../README.md) · [국제화·번역 설정](internationalization.md) · [Firebase 준비](firebase-setup.md) · [역할 마이그레이션](roles-migration.md)
|
|
4
|
+
|
|
5
|
+
기존 Nuxt 4 사이트에 게시판을 추가하는 절차다. 호스트는 라우트·Firebase 연결·규칙 배포를 맡고, memi-board는 게시판 컴포넌트와 composable을 제공한다.
|
|
6
|
+
|
|
7
|
+
이 가이드의 국제화·번역 옵션을 포함한 예제는 **memi-board 0.47.0 이상**을 기준으로 한다. 필수 버전은 Nuxt 4+, Nuxt UI 4+, Firebase 12+, Vue 3.5+, VueFire 3.2+다.
|
|
8
|
+
|
|
9
|
+
## 1. 필요한 패키지 설치
|
|
10
|
+
|
|
11
|
+
기존 Nuxt 프로젝트 루트에서 실행한다.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pnpm add memi-board@^0.47.0 @nuxt/ui nuxt-vuefire vuefire firebase
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`dayjs`는 필요 없다. `@nuxtjs/i18n`은 사이트 언어 전환이 필요할 때만 추가한다. 운영 환경에서는 검증한 memi-board 버전과 lockfile을 함께 관리한다.
|
|
18
|
+
|
|
19
|
+
## 2. 먼저 언어 모드 선택
|
|
20
|
+
|
|
21
|
+
| 서비스 | 권장 설정 |
|
|
22
|
+
|---|---|
|
|
23
|
+
| 한국어 공지·문의 게시판 | `locale: 'ko', i18n: { enabled: false }` |
|
|
24
|
+
| 영어 전용 서비스 | `locale: 'en', i18n: { enabled: false }` |
|
|
25
|
+
| 여러 언어 UI, 글은 작성한 언어 그대로 | `i18n: { enabled: true }, translation: { enabled: false }` |
|
|
26
|
+
| 게시판·글을 여러 언어로 번역 | `i18n: { enabled: true }, translation: { enabled: true }` |
|
|
27
|
+
|
|
28
|
+
설정 생략 시 두 기능은 모두 활성 상태다. 콘텐츠 번역에는 별도 Firebase AI Logic 준비가 필요하다. 자동 번역 토글의 의미와 실제 실행 시점은 [국제화·번역 설정](internationalization.md#4-번역이-실행되고-저장되는-시점)을 참고한다.
|
|
29
|
+
|
|
30
|
+
## 3. Nuxt 설정에 병합
|
|
31
|
+
|
|
32
|
+
다음은 **한국어 전용, AI 검열·번역을 사용하지 않는** 시작 예제다. 기존 `modules`, CSS, Firebase 설정이 있다면 중복 등록하지 않고 병합한다.
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
// nuxt.config.ts
|
|
36
|
+
export default defineNuxtConfig({
|
|
37
|
+
modules: ['@nuxt/ui', 'nuxt-vuefire', 'memi-board'],
|
|
38
|
+
css: ['~/assets/css/main.css'],
|
|
39
|
+
vuefire: {
|
|
40
|
+
config: {
|
|
41
|
+
apiKey: 'FIREBASE_WEB_API_KEY',
|
|
42
|
+
authDomain: 'YOUR_PROJECT.firebaseapp.com',
|
|
43
|
+
projectId: 'YOUR_PROJECT',
|
|
44
|
+
storageBucket: 'YOUR_ACTUAL_BUCKET',
|
|
45
|
+
messagingSenderId: 'YOUR_SENDER_ID',
|
|
46
|
+
appId: 'YOUR_WEB_APP_ID'
|
|
47
|
+
},
|
|
48
|
+
auth: { enabled: true }
|
|
49
|
+
},
|
|
50
|
+
memiBoard: {
|
|
51
|
+
locale: 'ko',
|
|
52
|
+
i18n: { enabled: false },
|
|
53
|
+
auth: { providers: ['google'] },
|
|
54
|
+
moderation: { enabled: false },
|
|
55
|
+
seo: {
|
|
56
|
+
siteName: '우리 서비스',
|
|
57
|
+
siteUrl: 'https://example.com',
|
|
58
|
+
basePath: '/board'
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
routeRules: {
|
|
62
|
+
'/board/**/new': { ssr: false },
|
|
63
|
+
'/board/**/edit': { ssr: false },
|
|
64
|
+
'/board-settings': { ssr: false },
|
|
65
|
+
'/board-settings/**': { ssr: false },
|
|
66
|
+
'/board-users': { ssr: false }
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Firebase 값은 **붙일 사이트의 Firebase 웹 앱 설정**을 사용한다. Storage 버킷 이름은 프로젝트마다 다를 수 있으므로 콘솔의 실제 값을 복사한다. Firebase Admin 서비스 계정 키는 클라이언트 설정에 넣지 않는다.
|
|
72
|
+
|
|
73
|
+
공개 목록·상세의 SSR은 유지한다. `moderation.enabled: false`여도 기본 로컬 금칙어 검사는 남는다. 글쓰기 AI의 교정·문장 다듬기까지 끄는 옵션은 이 설정에 포함되지 않으며, 해당 AI 메뉴를 사용할 때는 AI Logic 연결이 필요하다.
|
|
74
|
+
|
|
75
|
+
## 4. 스타일과 UApp 연결
|
|
76
|
+
|
|
77
|
+
```css
|
|
78
|
+
/* app/assets/css/main.css */
|
|
79
|
+
@import "tailwindcss";
|
|
80
|
+
@import "@nuxt/ui";
|
|
81
|
+
@source "../../../node_modules/memi-board/dist/runtime/components";
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`@source`는 위 CSS 파일 위치 기준이다. 다른 위치의 CSS를 쓰면 상대 경로를 맞춘다.
|
|
85
|
+
|
|
86
|
+
```vue
|
|
87
|
+
<!-- app/app.vue — 기존 UApp이 있으면 중복으로 감싸지 않는다 -->
|
|
88
|
+
<template>
|
|
89
|
+
<UApp>
|
|
90
|
+
<NuxtPage />
|
|
91
|
+
</UApp>
|
|
92
|
+
</template>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
레이아웃이 있는 앱은 기존 `<NuxtLayout>` 구조를 유지한다. Nuxt UI 내부 문구도 한국어로 고정하려면 `@nuxt/ui/locale`의 `ko`를 가져와 `<UApp :locale="ko">`로 지정한다. 호스트의 `<html lang>`도 호스트가 관리한다.
|
|
96
|
+
|
|
97
|
+
## 5. 규칙과 인덱스 배포
|
|
98
|
+
|
|
99
|
+
설치한 패키지의 다음 파일을 호스트 파일에 병합한다. 이미 사이트 규칙이 있으면 통째로 덮어쓰지 않는다.
|
|
100
|
+
|
|
101
|
+
| 패키지 파일 | 호스트 파일 |
|
|
102
|
+
|---|---|
|
|
103
|
+
| `node_modules/memi-board/docs/firestore.rules.example` | `firestore.rules` |
|
|
104
|
+
| `node_modules/memi-board/docs/storage.rules.example` | `storage.rules` |
|
|
105
|
+
| `node_modules/memi-board/docs/firestore.indexes.json.example` | `firestore.indexes.json` |
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
firebase deploy --only firestore:rules,storage,firestore:indexes
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`firebase.json`에는 인덱스 파일 경로도 지정한다. Storage 규칙의 배포 버킷은 `vuefire.config.storageBucket`과 같아야 한다. 교차 서비스 IAM까지 포함한 최초 설정은 [Firebase 준비](firebase-setup.md#storage-규칙의-firestore-조회-권한-부모호스트-프로젝트-필수-설정)를 따른다. npm 설치나 웹 사이트 배포가 이 배포를 대신하지 않는다.
|
|
112
|
+
|
|
113
|
+
## 6. 관리 페이지와 첫 게시판 만들기
|
|
114
|
+
|
|
115
|
+
```vue
|
|
116
|
+
<!-- app/pages/board-settings.vue -->
|
|
117
|
+
<template>
|
|
118
|
+
<MemiBoardSettings />
|
|
119
|
+
</template>
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
```vue
|
|
123
|
+
<!-- app/pages/board-users.vue -->
|
|
124
|
+
<template>
|
|
125
|
+
<MemiBoardUsers />
|
|
126
|
+
</template>
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
로그인용 페이지 또는 기존 계정 화면에 `<MemiBoardSignIn />`을 추가하고 다음 순서로 초기화한다.
|
|
130
|
+
|
|
131
|
+
1. Firebase Authentication에서 Google 공급자와 사이트의 승인 도메인을 설정한다.
|
|
132
|
+
2. 게시판의 로그인 컴포넌트로 로그인하여 `memiBoardUsers/{uid}` 문서를 생성한다. 호스트 자체 로그인만 있다면 게시판 인증 composable도 사용하는 페이지를 연다.
|
|
133
|
+
3. 신뢰할 수 있는 Firebase Console 또는 서버 Admin SDK에서 해당 문서의 `role`을 `owner`로 지정한다.
|
|
134
|
+
4. 새로고침 또는 재로그인 후 `/board-settings`에서 ID가 `free`인 게시판을 만든다.
|
|
135
|
+
5. `/board-users`에서 필요한 계정에 `admin`·`manager`·`staff` 역할을 지정하고, 게시판 설정에서 담당자를 배정한다.
|
|
136
|
+
|
|
137
|
+
최초 owner 지정과 owner 이관은 클라이언트 역할 선택 메뉴로 하지 않는다. 라우트 접근 가드는 호스트가 추가하고, 실제 데이터 접근은 규칙으로 검증한다. 호스트 로그인 공급자가 게시판과 같은 Firebase Auth를 사용하면 로그인 상태를 공유한다.
|
|
138
|
+
|
|
139
|
+
## 7. 목록·글쓰기·상세 페이지 연결
|
|
140
|
+
|
|
141
|
+
권장 경로는 다음과 같다. `/board` 시작 페이지와 메뉴 구성은 호스트가 정한다.
|
|
142
|
+
|
|
143
|
+
| 파일 | URL | 전달할 값 |
|
|
144
|
+
|---|---|---|
|
|
145
|
+
| `app/pages/board/[boardId]/index.vue` | `/board/free` | 목록의 `boardId` |
|
|
146
|
+
| `app/pages/board/[boardId]/new.vue` | `/board/free/new` | 에디터의 `boardId` |
|
|
147
|
+
| `app/pages/board/[boardId]/[slug]/index.vue` | `/board/free/글-slug` | slug를 조회해 상세의 `postId`로 전달 |
|
|
148
|
+
| `app/pages/board/[boardId]/[slug]/edit.vue` | `/board/free/글-slug/edit` | 조회한 문서 ID로 기존 글 수정 |
|
|
149
|
+
|
|
150
|
+
컴포넌트는 Nuxt 모듈이 auto-import한다. `memi-board/runtime`에서는 composable과 타입만 가져온다.
|
|
151
|
+
|
|
152
|
+
### 목록
|
|
153
|
+
|
|
154
|
+
```vue
|
|
155
|
+
<!-- app/pages/board/[boardId]/index.vue -->
|
|
156
|
+
<script setup lang="ts">
|
|
157
|
+
const route = useRoute()
|
|
158
|
+
const boardId = computed(() => String(route.params.boardId || ''))
|
|
159
|
+
const { isSignedIn, canConfigureBoard } = useMemiBoardAuth()
|
|
160
|
+
const { boardLabel } = useMemiBoardSettings()
|
|
161
|
+
await useMemiBoardListSeo({
|
|
162
|
+
category: boardId,
|
|
163
|
+
path: computed(() => `/board/${encodeURIComponent(boardId.value)}`)
|
|
164
|
+
})
|
|
165
|
+
</script>
|
|
166
|
+
|
|
167
|
+
<template>
|
|
168
|
+
<MemiBoardList
|
|
169
|
+
:board-id="boardId"
|
|
170
|
+
:title="boardLabel(boardId)"
|
|
171
|
+
:get-post-link="post => `/board/${encodeURIComponent(boardId)}/${encodeURIComponent(post.slug)}`"
|
|
172
|
+
:write-to="`/board/${boardId}/new`"
|
|
173
|
+
:can-write="isSignedIn"
|
|
174
|
+
:settings-to="`/board-settings/${boardId}`"
|
|
175
|
+
:can-manage-settings="canConfigureBoard(boardId)"
|
|
176
|
+
/>
|
|
177
|
+
<MemiBoardSignIn v-if="!isSignedIn" />
|
|
178
|
+
</template>
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
`canWrite`는 이 예제에서 로그인 여부만 표시한다. 스태프·관리자 전용 게시판을 운영할 때는 호스트가 게시판의 `writeRole`과 담당 범위까지 검사해 버튼을 표시한다. 규칙은 버튼 표시와 별개로 쓰기 권한을 검증한다.
|
|
182
|
+
|
|
183
|
+
목록의 개별 설정 링크에 대응하는 페이지도 만든다.
|
|
184
|
+
|
|
185
|
+
```vue
|
|
186
|
+
<!-- app/pages/board-settings/[boardId].vue -->
|
|
187
|
+
<script setup lang="ts">
|
|
188
|
+
const route = useRoute()
|
|
189
|
+
const boardId = computed(() => String(route.params.boardId || ''))
|
|
190
|
+
</script>
|
|
191
|
+
|
|
192
|
+
<template>
|
|
193
|
+
<MemiBoardCategorySettings :category-id="boardId" />
|
|
194
|
+
</template>
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
매니저 진입 가드에는 `canConfigureBoard(boardId)`, 콘텐츠 관리에는 `canManageBoard(boardId)`를 사용한다. 설정 컴포넌트도 기본적으로 담당 설정 권한을 확인한다.
|
|
198
|
+
|
|
199
|
+
### 글쓰기
|
|
200
|
+
|
|
201
|
+
```vue
|
|
202
|
+
<!-- app/pages/board/[boardId]/new.vue -->
|
|
203
|
+
<script setup lang="ts">
|
|
204
|
+
const route = useRoute()
|
|
205
|
+
const boardId = computed(() => String(route.params.boardId || ''))
|
|
206
|
+
const { isSignedIn, rolePending } = useMemiBoardAuth()
|
|
207
|
+
</script>
|
|
208
|
+
|
|
209
|
+
<template>
|
|
210
|
+
<MemiBoardEditor
|
|
211
|
+
v-if="!rolePending && isSignedIn"
|
|
212
|
+
:board-id="boardId"
|
|
213
|
+
@saved="slug => navigateTo(`/board/${boardId}/${encodeURIComponent(slug)}`)"
|
|
214
|
+
@cancel="navigateTo(`/board/${boardId}`)"
|
|
215
|
+
/>
|
|
216
|
+
<MemiBoardSignIn v-else-if="!rolePending" />
|
|
217
|
+
</template>
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
새 글의 `saved` 이벤트는 **slug**를 전달하고, 기존 글 수정의 `saved` 이벤트는 **Firestore 문서 ID**를 전달한다. 수정 페이지에서는 현재 URL의 slug를 유지해 이동한다. 에디터 자체가 게시판 언어를 적용한다.
|
|
221
|
+
|
|
222
|
+
### 상세
|
|
223
|
+
|
|
224
|
+
```vue
|
|
225
|
+
<!-- app/pages/board/[boardId]/[slug]/index.vue -->
|
|
226
|
+
<script setup lang="ts">
|
|
227
|
+
const route = useRoute()
|
|
228
|
+
const boardId = computed(() => String(route.params.boardId || ''))
|
|
229
|
+
const slug = computed(() => String(route.params.slug || ''))
|
|
230
|
+
const { post: seoPost } = await useMemiBoardPostSeo({
|
|
231
|
+
category: boardId,
|
|
232
|
+
slug,
|
|
233
|
+
path: computed(() => `/board/${encodeURIComponent(boardId.value)}/${encodeURIComponent(slug.value)}`)
|
|
234
|
+
})
|
|
235
|
+
const { resolvePostIdBySlug } = useMemiBoardPosts(boardId)
|
|
236
|
+
const postId = ref('')
|
|
237
|
+
const unavailable = ref(false)
|
|
238
|
+
watch([boardId, slug, seoPost], async ([board, currentSlug, seo], _, onCleanup) => {
|
|
239
|
+
let active = true
|
|
240
|
+
onCleanup(() => { active = false })
|
|
241
|
+
postId.value = ''
|
|
242
|
+
unavailable.value = false
|
|
243
|
+
if (seo?.id && seo.category === board && seo.slug === currentSlug) {
|
|
244
|
+
postId.value = seo.id
|
|
245
|
+
return
|
|
246
|
+
}
|
|
247
|
+
if (import.meta.server) return
|
|
248
|
+
const result = await resolvePostIdBySlug(currentSlug)
|
|
249
|
+
if (!active) return
|
|
250
|
+
postId.value = result.status === 'ok' ? result.id : ''
|
|
251
|
+
unavailable.value = result.status !== 'ok'
|
|
252
|
+
}, { immediate: true })
|
|
253
|
+
</script>
|
|
254
|
+
|
|
255
|
+
<template>
|
|
256
|
+
<MemiBoardDetail
|
|
257
|
+
v-if="postId"
|
|
258
|
+
:board-id="boardId"
|
|
259
|
+
:post-id="postId"
|
|
260
|
+
@edit="navigateTo(`/board/${boardId}/${encodeURIComponent(slug)}/edit`)"
|
|
261
|
+
@navigate="post => navigateTo(`/board/${boardId}/${encodeURIComponent(post.slug)}`)"
|
|
262
|
+
@list="navigateTo(`/board/${boardId}`)"
|
|
263
|
+
@deleted="navigateTo(`/board/${boardId}`)"
|
|
264
|
+
/>
|
|
265
|
+
<p v-else-if="unavailable">글을 찾을 수 없거나 접근 권한이 없습니다.</p>
|
|
266
|
+
<p v-else>불러오는 중입니다.</p>
|
|
267
|
+
</template>
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
`boardId`는 게시판 설정의 문서 ID, `postId`는 글의 Firestore 문서 ID, `slug`는 URL용 문자열이다. 상세 컴포넌트에 slug를 `postId`로 넣지 않는다. 위 예제의 공개 상세 SEO는 SSR에서 채우고, 초안·비공개 글의 클라이언트 조회는 로그인과 규칙을 따른다.
|
|
271
|
+
|
|
272
|
+
### 수정
|
|
273
|
+
|
|
274
|
+
```vue
|
|
275
|
+
<!-- app/pages/board/[boardId]/[slug]/edit.vue -->
|
|
276
|
+
<script setup lang="ts">
|
|
277
|
+
const route = useRoute()
|
|
278
|
+
const boardId = computed(() => String(route.params.boardId || ''))
|
|
279
|
+
const slug = computed(() => String(route.params.slug || ''))
|
|
280
|
+
const { rolePending, isSignedIn, canEdit } = useMemiBoardAuth()
|
|
281
|
+
const { resolvePostIdBySlug, getPost } = useMemiBoardPosts(boardId)
|
|
282
|
+
const postId = ref('')
|
|
283
|
+
const pending = ref(true)
|
|
284
|
+
watch([boardId, slug, rolePending, isSignedIn], async (_, __, onCleanup) => {
|
|
285
|
+
let active = true
|
|
286
|
+
onCleanup(() => { active = false })
|
|
287
|
+
postId.value = ''
|
|
288
|
+
pending.value = true
|
|
289
|
+
if (rolePending.value) return
|
|
290
|
+
if (isSignedIn.value) {
|
|
291
|
+
try {
|
|
292
|
+
const result = await resolvePostIdBySlug(slug.value)
|
|
293
|
+
const post = result.status === 'ok' ? await getPost(result.id) : null
|
|
294
|
+
if (active && post && canEdit(post)) postId.value = post.id ?? ''
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
// 로그인·권한·삭제·네트워크 오류는 아래 안내로 처리한다.
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (active) pending.value = false
|
|
301
|
+
}, { immediate: true })
|
|
302
|
+
</script>
|
|
303
|
+
|
|
304
|
+
<template>
|
|
305
|
+
<p v-if="pending">불러오는 중입니다.</p>
|
|
306
|
+
<MemiBoardEditor
|
|
307
|
+
v-else-if="postId"
|
|
308
|
+
:board-id="boardId"
|
|
309
|
+
:post-id="postId"
|
|
310
|
+
@saved="navigateTo(`/board/${boardId}/${encodeURIComponent(slug)}`)"
|
|
311
|
+
@cancel="navigateTo(`/board/${boardId}/${encodeURIComponent(slug)}`)"
|
|
312
|
+
/>
|
|
313
|
+
<p v-else>글을 수정할 권한이 없거나 글을 불러올 수 없습니다.</p>
|
|
314
|
+
</template>
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
## 8. 역할과 서비스별 명칭
|
|
318
|
+
|
|
319
|
+
```ts
|
|
320
|
+
memiBoard: {
|
|
321
|
+
roleLabels: {
|
|
322
|
+
user: '고객',
|
|
323
|
+
staff: '매장 직원',
|
|
324
|
+
manager: '매장 관리자',
|
|
325
|
+
admin: '운영자',
|
|
326
|
+
owner: '최고 관리자'
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
명칭만 바뀌며 권한은 바뀌지 않는다. manager는 `allowedManagerUids`에 지정된 게시판의 설정과 스태프 배정을 관리한다. 매장·조직과 게시판을 연결하고 담당 배정을 동기화하는 책임은 호스트에 있다. 단순히 같은 Firebase 프로젝트를 쓴다고 조직 경계가 자동 생성되지 않는다.
|
|
332
|
+
|
|
333
|
+
기본 파일 규칙에서 타인 파일 수정·삭제는 전역 admin/owner만 가능하다. 담당 staff/manager의 타인 첨부 교체·파일 정리가 필요한 서비스는 게시판 배정을 확인하는 서버 API를 추가한다. 기본 Storage 파일 읽기는 공개다. 자세한 역할과 기존 데이터 이전은 [역할 마이그레이션](roles-migration.md)을 따른다.
|
|
334
|
+
|
|
335
|
+
## 9. 다른 사이트와 데이터 분리
|
|
336
|
+
|
|
337
|
+
| 구성 | 권장 방식 |
|
|
338
|
+
|---|---|
|
|
339
|
+
| 별도 서비스·고객사의 독립 사이트 | Firebase 프로젝트도 분리 |
|
|
340
|
+
| 같은 서비스 안의 여러 게시판 | 같은 프로젝트에서 `boardId`로 구분 |
|
|
341
|
+
| 한 프로젝트 안의 다른 컬렉션 이름 | `postsCollection` 등 6개 이름과 Rules·Storage 경로·인덱스를 함께 변경 |
|
|
342
|
+
|
|
343
|
+
컬렉션 이름 옵션이나 `boardId`는 그 자체로 보안상 테넌트 격리를 보장하지 않는다. 기본 admin/owner는 해당 컬렉션 전체를 관리한다. 서로 독립적인 관리자 경계가 필요하면 프로젝트를 분리하거나 호스트 규칙을 별도로 설계한다.
|
|
344
|
+
|
|
345
|
+
## 10. 배포 전 확인
|
|
346
|
+
|
|
347
|
+
- 첫 owner 로그인 후 `free` 게시판 생성, 일반 계정의 글·댓글 작성 확인.
|
|
348
|
+
- 글 상세 진입 시 slug → 문서 ID 조회와 원문 표시 확인.
|
|
349
|
+
- 선택한 국제화 모드에 맞춰 언어·번역 UI와 AI 요청 여부 확인.
|
|
350
|
+
- 매니저가 담당 게시판만 설정하고 다른 게시판은 설정하지 못하는지 확인.
|
|
351
|
+
- 앱이 쓰는 Storage 버킷에 규칙이 배포됐는지 확인하고 이미지·첨부 업로드 확인.
|
|
352
|
+
- 공개 목록·상세 HTML에 OG 메타가 있는지 배포 URL에서 확인.
|
|
353
|
+
- 웹 앱 배포와 Firebase 규칙·인덱스 배포 결과를 각각 확인.
|
|
354
|
+
|
|
355
|
+
오류별 확인 순서는 [README 문제 해결](../README.md#문제-해결)을 참고한다.
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# 국제화와 콘텐츠 번역 설정
|
|
2
|
+
|
|
3
|
+
[설치 가이드](host-integration.md) · [README](../README.md) · [Firebase 준비](firebase-setup.md)
|
|
4
|
+
|
|
5
|
+
`i18n.enabled`·`translation.enabled`는 **memi-board 0.47.0 이상**에서 지원한다. 기존 호스트는 `pnpm add memi-board@^0.47.0`으로 업데이트하고 lockfile도 함께 반영한다.
|
|
6
|
+
|
|
7
|
+
## 1. 서비스에 맞는 모드 고르기
|
|
8
|
+
|
|
9
|
+
**UI 언어**는 저장·취소·안내문·날짜 같은 화면 문구의 언어다. **콘텐츠 번역**은 게시판 이름·설명, 섹션 제목, 글 제목·요약·본문을 다른 언어로 생성하고 표시하는 기능이다.
|
|
10
|
+
|
|
11
|
+
| 사용 목적 | `i18n.enabled` | `translation.enabled` | 동작 |
|
|
12
|
+
|---|---|---|---|
|
|
13
|
+
| 한국어 또는 영어만 사용하는 사이트 | `false` | 지정 불필요 | `locale`로 UI 고정, 콘텐츠 원문 표시, 번역 기능 숨김 |
|
|
14
|
+
| 다국어 UI만 필요한 사이트 | `true` | `false` | UI 언어 전환, 콘텐츠 원문 표시, 번역 기능 숨김 |
|
|
15
|
+
| 다국어 UI와 콘텐츠 번역이 필요한 사이트 | `true` | `true` | UI 언어 전환과 콘텐츠 번역 모두 사용 |
|
|
16
|
+
| 기존 설정 그대로 | 생략 | 생략 | 두 옵션 모두 기본 `true` |
|
|
17
|
+
|
|
18
|
+
`i18n.enabled: false`가 우선한다. 이때 `translation.enabled: true`를 지정해도 콘텐츠 번역은 실행하지 않는다. 두 기능을 끄더라도 기존 번역 데이터는 삭제하지 않는다.
|
|
19
|
+
|
|
20
|
+
## 2. 복사해서 사용할 설정
|
|
21
|
+
|
|
22
|
+
아래 `memiBoard` 블록을 호스트의 `defineNuxtConfig({ ... })` 안에 넣는다. Firebase 초기화 등 나머지 설정은 [설치 가이드](host-integration.md)를 따른다.
|
|
23
|
+
|
|
24
|
+
### 한 언어로만 운영
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
memiBoard: {
|
|
28
|
+
locale: 'ko', // 영어 전용이면 'en'
|
|
29
|
+
i18n: { enabled: false }
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
- 호스트의 현재 언어와 게시판별 언어를 무시하고 `locale`를 사용한다. 생략하거나 지원하지 않는 값이면 `ko`다.
|
|
34
|
+
- 게시판 언어 선택, 번역 버튼, 자동 번역 토글, 글쓰기 AI의 번역 메뉴를 숨긴다.
|
|
35
|
+
- 저장된 번역이 있어도 콘텐츠는 원문으로 표시한다.
|
|
36
|
+
- 게시판의 자동 번역 토글이 이전에 켜져 있어도 설정 저장 시 번역하지 않는다.
|
|
37
|
+
- `@nuxtjs/i18n`을 설치할 필요가 없다. 호스트가 이미 쓰고 있어도 게시판은 지정 언어로 고정된다.
|
|
38
|
+
|
|
39
|
+
### UI는 다국어, 콘텐츠는 원문
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
memiBoard: {
|
|
43
|
+
locale: 'ko',
|
|
44
|
+
i18n: { enabled: true },
|
|
45
|
+
translation: { enabled: false }
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
게시판 언어 선택은 유지하고 번역 관련 UI만 숨긴다. 메뉴·버튼·날짜는 현재 화면 언어를 따르고, 게시판 라벨·설명·섹션 제목·게시글은 작성한 원문을 보여 준다.
|
|
50
|
+
|
|
51
|
+
### UI와 콘텐츠 번역 모두 사용
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
memiBoard: {
|
|
55
|
+
locale: 'ko',
|
|
56
|
+
i18n: { enabled: true },
|
|
57
|
+
translation: { enabled: true }
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
번역을 생성하려면 호스트 Firebase 프로젝트에 AI Logic 연결을 준비해야 한다. UI 언어 파일을 보여 주는 것만으로는 AI 요청이 발생하지 않는다.
|
|
62
|
+
|
|
63
|
+
## 3. 호스트의 언어 전환 연결
|
|
64
|
+
|
|
65
|
+
지원 언어는 `ko`, `en`, `ja`, `de`, `fr`, `es`, `pt`, `zh`, `ar`, `id`다. `en-US`·`pt_BR` 같은 값은 지원 코드로 정규화한다. `i18n.enabled`가 켜져 있을 때 언어 결정 순서는 다음과 같다.
|
|
66
|
+
|
|
67
|
+
1. 게시판 설정의 고정 언어
|
|
68
|
+
2. 부모 게시판 화면이 전달한 언어
|
|
69
|
+
3. 호스트 `@nuxtjs/i18n`의 현재 언어
|
|
70
|
+
4. `memiBoard.locale`
|
|
71
|
+
5. `ko`
|
|
72
|
+
|
|
73
|
+
사이트 언어 전환을 그대로 따르려면 게시판 설정의 언어를 **자동**으로 둔다. 게시판 언어를 따로 고정하면 사이트 언어보다 우선한다.
|
|
74
|
+
|
|
75
|
+
호스트에 `@nuxtjs/i18n`을 사용하는 경우:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
// nuxt.config.ts — 기존 설정에 병합
|
|
79
|
+
modules: ['@nuxt/ui', '@nuxtjs/i18n', 'nuxt-vuefire', 'memi-board'],
|
|
80
|
+
i18n: {
|
|
81
|
+
locales: [
|
|
82
|
+
{ code: 'ko', language: 'ko-KR', name: '한국어', file: 'ko.json' },
|
|
83
|
+
{ code: 'en', language: 'en-US', name: 'English', file: 'en.json' }
|
|
84
|
+
],
|
|
85
|
+
defaultLocale: 'ko',
|
|
86
|
+
strategy: 'no_prefix',
|
|
87
|
+
langDir: 'locales/'
|
|
88
|
+
},
|
|
89
|
+
memiBoard: {
|
|
90
|
+
i18n: { enabled: true },
|
|
91
|
+
translation: { enabled: false }
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
이 예제는 한국어·영어 UI만 사용한다. 호스트의 `i18n/locales/ko.json`·`en.json`에 호스트 UI용 메시지를 만든다(처음에는 `{}`도 가능). 게시판 자체의 번역 파일은 패키지가 제공하므로 복사하지 않는다. 나머지 언어도 필요할 때 호스트 목록에 추가한다.
|
|
96
|
+
|
|
97
|
+
Nuxt UI 내부 문구와 사이트 `<html>` 언어는 호스트가 연결한다. 예를 들어 한국어·영어 호스트는 `app/app.vue`에서 다음처럼 설정할 수 있다.
|
|
98
|
+
|
|
99
|
+
```vue
|
|
100
|
+
<script setup lang="ts">
|
|
101
|
+
import { en, ko } from '@nuxt/ui/locale'
|
|
102
|
+
const { locale } = useI18n()
|
|
103
|
+
const uiLocale = computed(() => locale.value === 'en' ? en : ko)
|
|
104
|
+
useHead({ htmlAttrs: { lang: () => uiLocale.value.code, dir: () => uiLocale.value.dir } })
|
|
105
|
+
</script>
|
|
106
|
+
|
|
107
|
+
<template>
|
|
108
|
+
<UApp :locale="uiLocale">
|
|
109
|
+
<NuxtPage />
|
|
110
|
+
</UApp>
|
|
111
|
+
</template>
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
레이아웃을 사용하는 사이트는 기존 `<NuxtLayout>` 구조를 유지한다. 게시판이 단일 언어이고 호스트는 다국어인 경우, 사이트 전체의 `<html>`·`UApp` 언어는 호스트 정책대로 결정한다.
|
|
115
|
+
|
|
116
|
+
## 4. 번역이 실행되고 저장되는 시점
|
|
117
|
+
|
|
118
|
+
| 대상 | 실행 시점 | 저장 시점 |
|
|
119
|
+
|---|---|---|
|
|
120
|
+
| 게시판 라벨·설명: 수동 | **10개 언어로 번역** 클릭 | 번역 후 설정의 **저장** 클릭 |
|
|
121
|
+
| 게시판 라벨·설명: 자동 | 자동 번역 토글을 켜고 **저장** 클릭 | 번역 완료 후 설정과 함께 저장 |
|
|
122
|
+
| 홈 섹션 제목 | 섹션 레이아웃 저장 | 번역 후 레이아웃과 함께 저장 |
|
|
123
|
+
| 기존 글의 제목·요약·본문 | 글쓰기 AI의 **10개 언어로 번역해 저장** 클릭 | 번역 완료 후 해당 글에 즉시 저장 |
|
|
124
|
+
| 아직 저장하지 않은 새 글 | 글쓰기 AI의 **10개 언어로 번역해 저장** 클릭 | 새 글을 저장할 때 함께 저장 |
|
|
125
|
+
|
|
126
|
+
10개 언어에는 원문 언어가 포함된다. 원문이 한국어이면 나머지 9개 언어로 번역하고 한국어 원문을 합친다. 번역 버튼을 누르거나 저장할 때까지는 입력만으로 번역 요청을 보내지 않는다.
|
|
127
|
+
|
|
128
|
+
게시판 이름·설명과 섹션 제목이 번역 당시와 같고 번역이 모두 있으면 재번역을 건너뛴다. 게시판의 자동 번역 토글은 기본 꺼짐이다. `i18n.enabled`·`translation.enabled`의 기본 켜짐은 서비스에서 번역을 사용할 수 있다는 뜻이며, 게시판 자동 번역 토글을 자동으로 켜지는 않는다.
|
|
129
|
+
|
|
130
|
+
프로그램에서 `translateBoard()`를 직접 호출하면 현재 게시판 자동 번역 토글과 무관하게 번역할 수 있다. 서비스 전체의 국제화·번역 옵션이 꺼져 있으면 원문을 그대로 반환한다.
|
|
131
|
+
|
|
132
|
+
## 5. AI 연결과 실패 처리
|
|
133
|
+
|
|
134
|
+
[Firebase 준비 문서](firebase-setup.md)의 AI Logic·App Check 절차를 호스트 프로젝트에 적용한다. Firebase 웹 앱의 `projectId`, `appId`, App Check 등록 대상이 일치해야 한다. 번역 모델과 App Check 토큰 옵션은 기존 `memiBoard.moderation.model`·`moderation.useLimitedUseAppCheckTokens`를 공유한다.
|
|
135
|
+
|
|
136
|
+
`moderation.enabled: false`는 **검열**을 끄는 설정이다. 번역까지 끄려면 `translation.enabled: false` 또는 `i18n.enabled: false`를 사용한다. 반대로 번역을 꺼도 맞춤법 교정·문장 다듬기 같은 글쓰기 도우미와 검열은 각자의 설정을 따른다.
|
|
137
|
+
|
|
138
|
+
| 상황 | 처리 |
|
|
139
|
+
|---|---|
|
|
140
|
+
| 게시판 수동 번역 실패 | 입력 내용을 유지하고 오류 표시. 설정 저장은 사용자가 결정 |
|
|
141
|
+
| 게시판 자동 번역 실패 | 설정과 원문은 저장하고 번역 실패 안내 표시 |
|
|
142
|
+
| 섹션 번역 실패 | 원문 레이아웃 저장, 콘솔 경고 |
|
|
143
|
+
| 게시글 번역 실패 | 오류 표시. 번역 결과 저장 단계로 진행하지 않음 |
|
|
144
|
+
| 서비스 옵션으로 번역을 끈 상태 | 자동 번역을 건너뛰고 정상 저장. AI 번역 직접 호출은 `memi-board/translation-disabled` 오류 |
|
|
145
|
+
|
|
146
|
+
게시글 AI 번역은 언어 수와 무관하게 글쓰기 도우미 일일 사용량 1회로 처리한다. 번역 비활성화에 따른 호출 거절은 AI 요청이나 사용량 갱신 전에 이루어진다. 활성 상태에서 이미 시작한 요청의 실패는 사용량이 차감될 수 있다.
|
|
147
|
+
|
|
148
|
+
## 6. 기존 데이터와 다시 활성화하기
|
|
149
|
+
|
|
150
|
+
번역은 기존 문서의 `i18n`·`i18nSource`에 저장하며, 게시글 본문 번역은 해당 본문 문서에 저장한다. 기능을 끄기 위해 데이터 마이그레이션이나 번역 필드 삭제를 할 필요는 없다.
|
|
151
|
+
|
|
152
|
+
원문이 번역 당시와 다르면 해당 번역 대신 원문을 표시한다. 다시 활성화했을 때도 이 검사를 유지한다. 예를 들어 번역을 끈 동안 게시판 이름을 바꿨다면, 다시 번역하기 전까지 이전 이름의 번역을 보여 주지 않는다.
|
|
153
|
+
|
|
154
|
+
## 7. 호스트가 직접 만드는 UI
|
|
155
|
+
|
|
156
|
+
패키지는 자체 UI만 숨긴다. 호스트가 만든 언어 선택기나 번역 버튼은 호스트에서 아래 상태를 사용해 제어한다.
|
|
157
|
+
|
|
158
|
+
```vue
|
|
159
|
+
<script setup lang="ts">
|
|
160
|
+
import { useMemiBoardI18n } from 'memi-board/runtime'
|
|
161
|
+
const { i18nEnabled, translationEnabled } = useMemiBoardI18n()
|
|
162
|
+
</script>
|
|
163
|
+
|
|
164
|
+
<template>
|
|
165
|
+
<span v-if="i18nEnabled">언어 전환 가능</span>
|
|
166
|
+
<span v-if="translationEnabled">콘텐츠 번역 사용 가능</span>
|
|
167
|
+
</template>
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
컴포넌트 밖에서는 `isMemiBoardI18nEnabled()`·`isMemiBoardTranslationEnabled()`를 사용한다. 두 함수는 `memi-board/runtime`에서 명시적으로 가져온다. `configureMemiBoard()`도 같은 옵션을 받지만, Nuxt 호스트에서는 `nuxt.config.ts`의 `memiBoard`만 지정하면 된다.
|
|
171
|
+
|
|
172
|
+
## 8. 확인할 항목
|
|
173
|
+
|
|
174
|
+
- 단일 언어: 호스트를 다른 언어로 바꿔도 게시판 버튼·날짜가 지정 언어를 유지하는지 확인한다.
|
|
175
|
+
- 콘텐츠 번역만 끄기: UI는 바뀌고 기존 번역이 있는 글도 원문을 표시하는지 확인한다.
|
|
176
|
+
- 자동 번역이 켜진 게시판을 저장해도 번역 비활성화 상태에서는 AI 요청이 없는지 확인한다.
|
|
177
|
+
- 다시 활성화: 변경되지 않은 원문은 저장된 번역을 쓰고, 변경한 원문은 오래된 번역을 쓰지 않는지 확인한다.
|
|
178
|
+
- 번역 실패 시 입력과 설정이 보존되는지 확인한다.
|
|
179
|
+
|
|
180
|
+
패키지 개발 환경의 회귀 검사는 `pnpm test:i18n`으로 실행한다.
|
package/docs/roles-migration.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# 0.46 역할 마이그레이션
|
|
2
2
|
|
|
3
|
+
[호스트 설치 가이드](host-integration.md) · [국제화·번역 설정](internationalization.md) · [Firebase 준비](firebase-setup.md)
|
|
4
|
+
|
|
5
|
+
신규 설치는 로그인으로 생성된 사용자 문서의 역할을 신뢰할 수 있는 콘솔 또는 서버에서 `owner`로 지정한다. 아래 admin → owner·staff → manager 이전은 기존 3개 역할을 쓰던 호스트에만 필요하다. 새로 만든 admin을 반복해서 owner로 바꾸는 작업이 아니다.
|
|
6
|
+
|
|
3
7
|
역할: user(일반 사용자), staff(콘텐츠 담당), manager(담당 게시판 설정·스태프 배정), admin(전체 관리), owner(관리자 지정).
|
|
4
8
|
|
|
5
9
|
`allowedStaffUids`는 콘텐츠 담당자, `allowedManagerUids`는 설정 담당자다. manager도 다른 게시판의 allowedStaffUids에만 배정되면 그 게시판에서는 콘텐츠만 관리한다. 글로벌 역할은 자격이고 실제 범위는 게시판 문서가 정한다. 매장 등 호스트의 조직을 게시판에 연결하고 배정을 동기화하는 책임은 호스트에 있다.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memi-board",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.47.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Nuxt 4 + Nuxt UI + VueFire/Firebase 전용 게시판 모듈",
|
|
6
6
|
"license": "MIT",
|
|
@@ -60,7 +60,8 @@
|
|
|
60
60
|
"dev": "pnpm build && nuxi dev playground",
|
|
61
61
|
"dev:build": "pnpm build && nuxi build playground",
|
|
62
62
|
"dev:prepare": "pnpm build && nuxi prepare playground",
|
|
63
|
-
"typecheck": "vue-tsc --noEmit"
|
|
63
|
+
"typecheck": "vue-tsc --noEmit",
|
|
64
|
+
"test:i18n": "pnpm build && node --test tests/i18n-options.test.mjs"
|
|
64
65
|
},
|
|
65
66
|
"peerDependencies": {
|
|
66
67
|
"@nuxt/ui": "^4.0.0",
|