Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 20955e277c |
@@ -1,5 +1,8 @@
|
||||
# 의사결정 이력
|
||||
|
||||
## 2026-04-02 v1.4.6
|
||||
- 내부 리네이밍 2단계는 관리자 화면처럼 상태와 액션이 많은 영역부터 정리해 두는 편이, 이후 `/games` 라우트와 API 계층을 손볼 때 위험을 줄이는 데 더 유리하다고 판단했다.
|
||||
|
||||
## 2026-04-02 v1.4.5
|
||||
- 내부 리네이밍은 한 번에 API와 DB까지 건드리기보다, 홈·주제 화면·에디터처럼 영향 범위가 비교적 명확한 프런트 핵심 흐름부터 `game` 의존 이름을 줄여 나가는 편이 더 안전하다고 판단했다.
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# 할 일 및 이슈
|
||||
|
||||
## 단기 확인
|
||||
- 내부 리네이밍 1단계로 홈/주제 화면/에디터/App 셸의 로컬 상태명은 먼저 정리했으므로, 다음 단계에서는 관리자 내부 `selectedGame / selectedGameId / games` 묶음을 같은 기준으로 옮긴다.
|
||||
- 화면/문서 층의 용어 정리는 거의 마무리됐으므로, 다음 단계에서는 내부 `gameId / games` 모델을 정말 `topic / template` 쪽으로 옮길지 범위를 먼저 정리한다.
|
||||
- 내부 리네이밍 2단계로 관리자 `selectedTemplate / templates / loadTemplate / refreshTemplates` 묶음까지 정리했으므로, 다음 단계에서는 `/games/:gameId` 라우트와 프런트 API 호출부를 어디까지 `topic/template` 의미로 감쌀지 범위를 먼저 정리한다.
|
||||
- 화면/문서 층의 용어 정리는 거의 마무리됐으므로, 다음 단계에서는 내부 `gameId / games` 모델을 실제로 옮길지, 아니면 라우트 alias/리다이렉트부터 둘지 점진 전환 순서를 정한다.
|
||||
- 관리자 화면까지 포함한 사용자 노출 `게임` 문구를 3차까지 정리했으므로, 실제 운영 흐름에서 뜨는 확인창/토스트/선택 모달까지 표현이 자연스러운지 한 번 더 QA한다.
|
||||
- 왼쪽 사이드 메뉴와 각 화면 타이틀을 한글 기준으로 맞췄으므로, 홈/나의 티어표/즐겨찾기/설정 진입 시 실제 체감이 자연스럽고 중복 표현이 어색하지 않은지 한 번 더 QA한다.
|
||||
- 사용자 노출 용어는 `주제 / 템플릿` 기준으로 계속 걷어내고 있으므로, 홈/주제 화면/관리자 템플릿 관리에서 어색하게 남은 `게임` 문구가 없는지 한 번 더 QA한다.
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# 업데이트 로그
|
||||
|
||||
## 2026-04-02 v1.4.6
|
||||
- 관리자 내부 리네이밍 2단계로 `AdminView`와 관련 composable/component의 핵심 상태명을 `selectedTemplate / templates / loadTemplate / refreshTemplates / createTemplate` 기준으로 정리했다.
|
||||
- 요청 검토, 템플릿 생성 모달, 아이템 추가/정렬, 템플릿 선택 모달 흐름도 같은 기준으로 맞춰, 관리자 화면을 읽을 때 내부 이름과 사용자 노출 용어가 덜 어긋나게 정리했다.
|
||||
|
||||
## 2026-04-02 v1.4.5
|
||||
- 내부 리네이밍 1단계를 시작해 홈, 주제 화면, 티어표 편집기, 앱 셸에서 `games / gameId / gameName` 중심의 로컬 상태명을 `templates / topicId / templateId / templateName` 계열로 먼저 정리했다.
|
||||
- 경로와 API는 그대로 둔 채 프런트 내부에서 자주 읽는 상태명부터 바꿔, 이후 `/games` 라우트와 관리자 상태를 손볼 때 의미 충돌이 덜 나도록 기반을 만들었다.
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
featuredGames: { type: Array, required: true },
|
||||
availableGamesForFeatured: { type: Array, required: true },
|
||||
featuredGameIds: { type: Array, required: true },
|
||||
featuredTemplates: { type: Array, required: true },
|
||||
availableTemplatesForFeatured: { type: Array, required: true },
|
||||
featuredTemplateIds: { type: Array, required: true },
|
||||
featuredListRef: { type: Function, required: true },
|
||||
saveFeaturedOrder: { type: Function, required: true },
|
||||
moveFeaturedGame: { type: Function, required: true },
|
||||
removeFeaturedGame: { type: Function, required: true },
|
||||
addFeaturedGame: { type: Function, required: true },
|
||||
moveFeaturedTemplate: { type: Function, required: true },
|
||||
removeFeaturedTemplate: { type: Function, required: true },
|
||||
addFeaturedTemplate: { type: Function, required: true },
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -24,21 +24,21 @@ const props = defineProps({
|
||||
<div class="featuredOrderPanel">
|
||||
<div class="featuredOrderPanel__list">
|
||||
<div class="section__title">상단 고정 목록</div>
|
||||
<div v-if="!props.featuredGames.length" class="hint">아직 상단 고정 템플릿이 없어요.</div>
|
||||
<div v-if="!props.featuredTemplates.length" class="hint">아직 상단 고정 템플릿이 없어요.</div>
|
||||
<div v-else :ref="props.featuredListRef" class="featuredList">
|
||||
<article v-for="(game, index) in props.featuredGames" :key="game.id" class="featuredCard" :data-featured-id="game.id">
|
||||
<article v-for="(template, index) in props.featuredTemplates" :key="template.id" class="featuredCard" :data-featured-id="template.id">
|
||||
<div class="featuredCard__meta">
|
||||
<span class="featuredCard__rank">{{ index + 1 }}</span>
|
||||
<div>
|
||||
<div class="featuredCard__title">{{ game.name }}</div>
|
||||
<div class="featuredCard__id">{{ game.id }}</div>
|
||||
<div class="featuredCard__title">{{ template.name }}</div>
|
||||
<div class="featuredCard__id">{{ template.id }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="featuredCard__actions">
|
||||
<button class="btn btn--ghost btn--small" data-featured-handle>드래그</button>
|
||||
<button class="btn btn--ghost btn--small" :disabled="index === 0" @click="props.moveFeaturedGame(game.id, -1)">위로</button>
|
||||
<button class="btn btn--ghost btn--small" :disabled="index === props.featuredGames.length - 1" @click="props.moveFeaturedGame(game.id, 1)">아래로</button>
|
||||
<button class="btn btn--danger btn--small" @click="props.removeFeaturedGame(game.id)">제외</button>
|
||||
<button class="btn btn--ghost btn--small" :disabled="index === 0" @click="props.moveFeaturedTemplate(template.id, -1)">위로</button>
|
||||
<button class="btn btn--ghost btn--small" :disabled="index === props.featuredTemplates.length - 1" @click="props.moveFeaturedTemplate(template.id, 1)">아래로</button>
|
||||
<button class="btn btn--danger btn--small" @click="props.removeFeaturedTemplate(template.id)">제외</button>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
@@ -48,14 +48,14 @@ const props = defineProps({
|
||||
<div class="section__title">템플릿 추가</div>
|
||||
<div class="featuredPickerList">
|
||||
<button
|
||||
v-for="game in props.availableGamesForFeatured"
|
||||
:key="game.id"
|
||||
v-for="template in props.availableTemplatesForFeatured"
|
||||
:key="template.id"
|
||||
class="featuredPickerItem"
|
||||
:disabled="props.featuredGameIds.length >= 50"
|
||||
@click="props.addFeaturedGame(game.id)"
|
||||
:disabled="props.featuredTemplateIds.length >= 50"
|
||||
@click="props.addFeaturedTemplate(template.id)"
|
||||
>
|
||||
<span>{{ game.name }}</span>
|
||||
<span class="featuredPickerItem__id">{{ game.id }}</span>
|
||||
<span>{{ template.name }}</span>
|
||||
<span class="featuredPickerItem__id">{{ template.id }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,10 +8,10 @@ const props = defineProps({
|
||||
templateRequestSourceUrl: { type: Function, required: true },
|
||||
stagedRequestDraftCount: { type: Number, required: true },
|
||||
appliedRequestItemCount: { type: Number, required: true },
|
||||
openGameCreateModal: { type: Function, required: true },
|
||||
openTemplateCreateModal: { type: Function, required: true },
|
||||
isGameLoading: { type: Boolean, required: true },
|
||||
hasSelectedGame: { type: Boolean, required: true },
|
||||
selectedGame: { type: Object, default: null },
|
||||
hasSelectedTemplate: { type: Boolean, required: true },
|
||||
selectedTemplate: { type: Object, default: null },
|
||||
displayThumbnailUrl: { type: String, default: '' },
|
||||
canApplyThumbnail: { type: Boolean, required: true },
|
||||
gameVisibilitySaving: { type: Boolean, required: true },
|
||||
@@ -24,8 +24,8 @@ const props = defineProps({
|
||||
onThumbDrop: { type: Function, required: true },
|
||||
isThumbDragOver: { type: Boolean, required: true },
|
||||
uploadThumbnail: { type: Function, required: true },
|
||||
removeGame: { type: Function, required: true },
|
||||
toggleSelectedGameVisibility: { type: Function, required: true },
|
||||
removeTemplate: { type: Function, required: true },
|
||||
toggleSelectedTemplateVisibility: { type: Function, required: true },
|
||||
itemFileInputRef: { type: Function, required: true },
|
||||
onFile: { type: Function, required: true },
|
||||
isItemDragOver: { type: Boolean, required: true },
|
||||
@@ -39,12 +39,12 @@ const props = defineProps({
|
||||
canAddItem: { type: Boolean, required: true },
|
||||
uploadItem: { type: Function, required: true },
|
||||
removeUploadDraft: { type: Function, required: true },
|
||||
hasGameItemOrderChanges: { type: Boolean, required: true },
|
||||
saveGameItemOrder: { type: Function, required: true },
|
||||
hasTemplateItemOrderChanges: { type: Boolean, required: true },
|
||||
saveTemplateItemOrder: { type: Function, required: true },
|
||||
gameItemListRef: { type: Function, required: true },
|
||||
saveGameItemLabel: { type: Function, required: true },
|
||||
removeGameItem: { type: Function, required: true },
|
||||
selectedGameId: { type: String, default: '' },
|
||||
saveTemplateItemLabel: { type: Function, required: true },
|
||||
removeTemplateItem: { type: Function, required: true },
|
||||
selectedTemplateId: { type: String, default: '' },
|
||||
})
|
||||
|
||||
function setGameItemListElement(el) {
|
||||
@@ -95,7 +95,7 @@ function setThumbFileElement(el) {
|
||||
v-if="props.activeTemplateRequest.type === 'create' && !props.activeTemplateRequest.targetGameId"
|
||||
class="btn btn--ghost btn--small"
|
||||
type="button"
|
||||
@click="props.openGameCreateModal"
|
||||
@click="props.openTemplateCreateModal"
|
||||
>
|
||||
새 템플릿 만들기
|
||||
</button>
|
||||
@@ -108,7 +108,7 @@ function setThumbFileElement(el) {
|
||||
<div class="emptyState__desc">선택한 템플릿의 썸네일과 기본 아이템을 곧 표시합니다.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="props.hasSelectedGame" class="panel">
|
||||
<div v-else-if="props.hasSelectedTemplate" class="panel">
|
||||
<section class="adminCard gameSettingsCard">
|
||||
<div class="gameSettingsCard__media">
|
||||
<input :ref="setThumbFileElement" type="file" accept="image/*" class="srOnlyInput" @change="props.onThumb" />
|
||||
@@ -122,7 +122,7 @@ function setThumbFileElement(el) {
|
||||
@dragleave="props.onThumbDragLeave"
|
||||
@drop="props.onThumbDrop"
|
||||
>
|
||||
<img v-if="props.displayThumbnailUrl" class="selectedThumb selectedThumb--sidebar" :src="props.displayThumbnailUrl" :alt="props.selectedGame.game.name" />
|
||||
<img v-if="props.displayThumbnailUrl" class="selectedThumb selectedThumb--sidebar" :src="props.displayThumbnailUrl" :alt="props.selectedTemplate.game.name" />
|
||||
<div v-else class="selectedThumb selectedThumb--empty selectedThumb--sidebar">대표 썸네일</div>
|
||||
<div class="thumbDropZone__copy">
|
||||
<div class="thumbDropZone__iconWrap">
|
||||
@@ -134,15 +134,15 @@ function setThumbFileElement(el) {
|
||||
</div>
|
||||
<div class="gameSettingsCard__body">
|
||||
<div class="panel__title">템플릿 설정</div>
|
||||
<div class="gameSettingsCard__meta">{{ props.selectedGame.game.name }} · {{ props.selectedGame.game.id }}</div>
|
||||
<div class="gameSettingsCard__meta">{{ props.selectedTemplate.game.name }} · {{ props.selectedTemplate.game.id }}</div>
|
||||
<label class="toggleSwitch" :class="{ 'toggleSwitch--disabled': props.gameVisibilitySaving }">
|
||||
<input :checked="!!props.selectedGame.game.isPublic" type="checkbox" @change="props.toggleSelectedGameVisibility($event.target.checked)" />
|
||||
<span class="toggleSwitch__label">{{ props.selectedGame.game.isPublic ? '템플릿 공개중' : '비공개 상태' }}</span>
|
||||
<input :checked="!!props.selectedTemplate.game.isPublic" type="checkbox" @change="props.toggleSelectedTemplateVisibility($event.target.checked)" />
|
||||
<span class="toggleSwitch__label">{{ props.selectedTemplate.game.isPublic ? '템플릿 공개중' : '비공개 상태' }}</span>
|
||||
<span class="toggleSwitch__track"><span class="toggleSwitch__thumb"></span></span>
|
||||
</label>
|
||||
<div class="gameSettingsCard__actions">
|
||||
<button class="btn" :disabled="!props.canApplyThumbnail" @click="props.uploadThumbnail">썸네일 적용</button>
|
||||
<button class="btn btn--danger" @click="props.removeGame">템플릿 삭제</button>
|
||||
<button class="btn btn--danger" @click="props.removeTemplate">템플릿 삭제</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -212,11 +212,11 @@ function setThumbFileElement(el) {
|
||||
<div class="section__title">현재 기본 아이템 목록</div>
|
||||
<div class="hint hint--tight">드래그해서 기본 노출 순서를 바꿀 수 있어요. 저장 전까지는 화면에서만 순서가 바뀝니다.</div>
|
||||
</div>
|
||||
<button class="btn btn--primary btn--small" :disabled="!props.hasGameItemOrderChanges" @click="props.saveGameItemOrder">순서 저장</button>
|
||||
<button class="btn btn--primary btn--small" :disabled="!props.hasTemplateItemOrderChanges" @click="props.saveTemplateItemOrder">순서 저장</button>
|
||||
</div>
|
||||
<div v-if="!props.selectedGame?.items?.length" class="hint">아직 등록된 기본 아이템이 없어요.</div>
|
||||
<div v-if="!props.selectedTemplate?.items?.length" class="hint">아직 등록된 기본 아이템이 없어요.</div>
|
||||
<div v-else :ref="setGameItemListElement" class="thumbGrid">
|
||||
<div v-for="item in props.selectedGame.items" :key="item.id" class="thumbCard" :data-game-item-id="item.id">
|
||||
<div v-for="item in props.selectedTemplate.items" :key="item.id" class="thumbCard" :data-game-item-id="item.id">
|
||||
<img class="thumb thumb--game" :src="toApiUrl(item.src)" :alt="item.label" draggable="false" />
|
||||
<input v-model="item.draftLabel" class="input input--labelEdit" placeholder="아이템 이름" data-no-drag />
|
||||
<div class="thumbCard__actions">
|
||||
@@ -224,11 +224,11 @@ function setThumbFileElement(el) {
|
||||
class="btn btn--ghost btn--small"
|
||||
data-no-drag
|
||||
:disabled="item.isSavingLabel || !item.draftLabel?.trim() || item.draftLabel.trim() === item.label"
|
||||
@click="props.saveGameItemLabel(item)"
|
||||
@click="props.saveTemplateItemLabel(item)"
|
||||
>
|
||||
{{ item.isSavingLabel ? '저장중...' : '이름 저장' }}
|
||||
</button>
|
||||
<button class="btn btn--danger btn--small" data-no-drag @click="props.removeGameItem(item.id)">아이템 삭제</button>
|
||||
<button class="btn btn--danger btn--small" data-no-drag @click="props.removeTemplateItem(item.id)">아이템 삭제</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -238,7 +238,7 @@ function setThumbFileElement(el) {
|
||||
<div class="emptyState">
|
||||
<div class="emptyState__title">템플릿을 선택해 주세요.</div>
|
||||
<div v-if="props.activeTemplateRequest?.type === 'create'" class="hint hint--tight">진행 중인 신규 템플릿 요청이 있어요. 위의 `새 템플릿 만들기`로 템플릿을 만든 뒤 아이템을 추가할 수 있습니다.</div>
|
||||
<div v-if="props.selectedGameId" class="hint hint--tight">선택한 템플릿을 찾지 못했거나 로딩 중 오류가 발생했어요. 다시 선택해보세요.</div>
|
||||
<div v-if="props.selectedTemplateId" class="hint hint--tight">선택한 템플릿을 찾지 못했거나 로딩 중 오류가 발생했어요. 다시 선택해보세요.</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -15,13 +15,13 @@ export function useAdminCustomItems({
|
||||
modalTargetCustomItem,
|
||||
customItemModalDraftLabel,
|
||||
customItemModalLabelSaving,
|
||||
customItemModalTargetGameId,
|
||||
games,
|
||||
selectedGameId,
|
||||
customItemModalTargetTemplateId,
|
||||
templates,
|
||||
selectedTemplateId,
|
||||
refreshCustomItems,
|
||||
loadGame,
|
||||
loadTemplate,
|
||||
setTab,
|
||||
selectAdminGame,
|
||||
selectAdminTemplate,
|
||||
resetMessages,
|
||||
success,
|
||||
error,
|
||||
@@ -59,7 +59,7 @@ export function useAdminCustomItems({
|
||||
function openCustomItemModal(item) {
|
||||
modalTargetCustomItem.value = item || null
|
||||
customItemModalDraftLabel.value = item?.label || ''
|
||||
customItemModalTargetGameId.value = ''
|
||||
customItemModalTargetTemplateId.value = ''
|
||||
customItemModalOpen.value = true
|
||||
pushCustomItemModalHistoryState()
|
||||
}
|
||||
@@ -70,7 +70,7 @@ export function useAdminCustomItems({
|
||||
modalTargetCustomItem.value = null
|
||||
customItemModalDraftLabel.value = ''
|
||||
customItemModalLabelSaving.value = false
|
||||
customItemModalTargetGameId.value = ''
|
||||
customItemModalTargetTemplateId.value = ''
|
||||
|
||||
if (fromPopState) {
|
||||
customItemModalHistoryActive.value = false
|
||||
@@ -97,12 +97,12 @@ export function useAdminCustomItems({
|
||||
customItemDeleteModalOpen.value = false
|
||||
}
|
||||
|
||||
function jumpToGameAdmin(gameId) {
|
||||
if (!gameId) return
|
||||
function jumpToTemplateAdmin(templateId) {
|
||||
if (!templateId) return
|
||||
closeCustomItemModal()
|
||||
setTab('game-admin')
|
||||
nextTick(() => {
|
||||
selectAdminGame(gameId)
|
||||
selectAdminTemplate(templateId)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -160,18 +160,19 @@ export function useAdminCustomItems({
|
||||
|
||||
async function promoteCustomItem(item) {
|
||||
resetMessages()
|
||||
if (!customItemModalTargetGameId.value) {
|
||||
if (!customItemModalTargetTemplateId.value) {
|
||||
error.value = '추가할 템플릿을 먼저 선택해주세요.'
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
item.isPromoting = true
|
||||
await api.promoteAdminCustomItem(item.id, { gameId: customItemModalTargetGameId.value })
|
||||
const targetGameName = games.value.find((game) => game.id === customItemModalTargetGameId.value)?.name || customItemModalTargetGameId.value
|
||||
if (selectedGameId.value === customItemModalTargetGameId.value) await loadGame()
|
||||
await api.promoteAdminCustomItem(item.id, { gameId: customItemModalTargetTemplateId.value })
|
||||
const targetTemplateName =
|
||||
templates.value.find((template) => template.id === customItemModalTargetTemplateId.value)?.name || customItemModalTargetTemplateId.value
|
||||
if (selectedTemplateId.value === customItemModalTargetTemplateId.value) await loadTemplate()
|
||||
closeCustomItemModal()
|
||||
success.value = `"${item.label}" 이미지를 ${targetGameName} 템플릿으로 추가했어요.`
|
||||
success.value = `"${item.label}" 이미지를 ${targetTemplateName} 템플릿으로 추가했어요.`
|
||||
} catch (e) {
|
||||
error.value = '선택한 이미지를 템플릿으로 추가하지 못했어요.'
|
||||
} finally {
|
||||
@@ -189,7 +190,7 @@ export function useAdminCustomItems({
|
||||
closeCustomItemModal,
|
||||
openCustomItemDeleteModal,
|
||||
closeCustomItemDeleteModal,
|
||||
jumpToGameAdmin,
|
||||
jumpToTemplateAdmin,
|
||||
removeCustomItem,
|
||||
removeUnusedCustomItems,
|
||||
saveCustomItemModalLabel,
|
||||
|
||||
@@ -5,8 +5,8 @@ export function useAdminFeaturedGames({
|
||||
api,
|
||||
featuredListEl,
|
||||
featuredSortable,
|
||||
featuredGameIds,
|
||||
games,
|
||||
featuredTemplateIds,
|
||||
templates,
|
||||
resetMessages,
|
||||
success,
|
||||
error,
|
||||
@@ -31,51 +31,51 @@ export function useAdminFeaturedGames({
|
||||
chosenClass: 'chosen',
|
||||
onEnd: (evt) => {
|
||||
if (evt.oldIndex == null || evt.newIndex == null || evt.oldIndex === evt.newIndex) return
|
||||
const nextIds = [...featuredGameIds.value]
|
||||
const nextIds = [...featuredTemplateIds.value]
|
||||
const [moved] = nextIds.splice(evt.oldIndex, 1)
|
||||
nextIds.splice(evt.newIndex, 0, moved)
|
||||
featuredGameIds.value = nextIds
|
||||
featuredTemplateIds.value = nextIds
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function addFeaturedGame(gameId) {
|
||||
function addFeaturedTemplate(templateId) {
|
||||
resetMessages()
|
||||
if (!gameId || featuredGameIds.value.includes(gameId)) return
|
||||
if (featuredGameIds.value.length >= 50) {
|
||||
if (!templateId || featuredTemplateIds.value.includes(templateId)) return
|
||||
if (featuredTemplateIds.value.length >= 50) {
|
||||
error.value = '상단 고정 템플릿은 최대 50개까지만 설정할 수 있어요.'
|
||||
return
|
||||
}
|
||||
featuredGameIds.value = [...featuredGameIds.value, gameId]
|
||||
featuredTemplateIds.value = [...featuredTemplateIds.value, templateId]
|
||||
syncFeaturedSortable()
|
||||
}
|
||||
|
||||
function removeFeaturedGame(gameId) {
|
||||
function removeFeaturedTemplate(templateId) {
|
||||
resetMessages()
|
||||
featuredGameIds.value = featuredGameIds.value.filter((id) => id !== gameId)
|
||||
featuredTemplateIds.value = featuredTemplateIds.value.filter((id) => id !== templateId)
|
||||
syncFeaturedSortable()
|
||||
}
|
||||
|
||||
function moveFeaturedGame(gameId, direction) {
|
||||
const currentIndex = featuredGameIds.value.indexOf(gameId)
|
||||
function moveFeaturedTemplate(templateId, direction) {
|
||||
const currentIndex = featuredTemplateIds.value.indexOf(templateId)
|
||||
const nextIndex = currentIndex + direction
|
||||
if (currentIndex < 0 || nextIndex < 0 || nextIndex >= featuredGameIds.value.length) return
|
||||
const nextIds = [...featuredGameIds.value]
|
||||
if (currentIndex < 0 || nextIndex < 0 || nextIndex >= featuredTemplateIds.value.length) return
|
||||
const nextIds = [...featuredTemplateIds.value]
|
||||
const [moved] = nextIds.splice(currentIndex, 1)
|
||||
nextIds.splice(nextIndex, 0, moved)
|
||||
featuredGameIds.value = nextIds
|
||||
featuredTemplateIds.value = nextIds
|
||||
syncFeaturedSortable()
|
||||
}
|
||||
|
||||
async function saveFeaturedOrder() {
|
||||
resetMessages()
|
||||
try {
|
||||
const data = await api.updateAdminGameDisplayOrder({ gameIds: featuredGameIds.value })
|
||||
games.value = data.games || []
|
||||
featuredGameIds.value = games.value
|
||||
.filter((game) => game.displayRank != null)
|
||||
const data = await api.updateAdminGameDisplayOrder({ gameIds: featuredTemplateIds.value })
|
||||
templates.value = data.games || []
|
||||
featuredTemplateIds.value = templates.value
|
||||
.filter((template) => template.displayRank != null)
|
||||
.sort((a, b) => a.displayRank - b.displayRank)
|
||||
.map((game) => game.id)
|
||||
.map((template) => template.id)
|
||||
success.value = '홈 화면 템플릿 순서를 저장했어요.'
|
||||
} catch (e) {
|
||||
error.value = '템플릿 순서 저장에 실패했어요.'
|
||||
@@ -85,9 +85,9 @@ export function useAdminFeaturedGames({
|
||||
return {
|
||||
destroyFeaturedSortable,
|
||||
syncFeaturedSortable,
|
||||
addFeaturedGame,
|
||||
removeFeaturedGame,
|
||||
moveFeaturedGame,
|
||||
addFeaturedTemplate,
|
||||
removeFeaturedTemplate,
|
||||
moveFeaturedTemplate,
|
||||
saveFeaturedOrder,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ import Sortable from 'sortablejs'
|
||||
export function useAdminGameManager({
|
||||
api,
|
||||
toApiUrl,
|
||||
selectedGameId,
|
||||
selectedGame,
|
||||
selectedTemplateId,
|
||||
selectedTemplate,
|
||||
uploadFiles,
|
||||
uploadItemDrafts,
|
||||
thumbFile,
|
||||
@@ -18,15 +18,15 @@ export function useAdminGameManager({
|
||||
activeTemplateRequest,
|
||||
templateRequests,
|
||||
customItemModalOpen,
|
||||
customItemModalTargetGameId,
|
||||
newGameId,
|
||||
newGameName,
|
||||
newGameIsPublic,
|
||||
customItemModalTargetTemplateId,
|
||||
newTemplateId,
|
||||
newTemplateName,
|
||||
newTemplateIsPublic,
|
||||
clearPreviewUrl,
|
||||
resetFileInput,
|
||||
resetUploadState,
|
||||
refreshGames,
|
||||
closeGameCreateModal,
|
||||
refreshTemplates,
|
||||
closeTemplateCreateModal,
|
||||
resetMessages,
|
||||
success,
|
||||
error,
|
||||
@@ -59,7 +59,7 @@ export function useAdminGameManager({
|
||||
async function syncGameItemSortable() {
|
||||
await nextTick()
|
||||
destroyGameItemSortable()
|
||||
if (!gameItemListEl.value || !selectedGame.value?.items?.length) return
|
||||
if (!gameItemListEl.value || !selectedTemplate.value?.items?.length) return
|
||||
|
||||
gameItemSortable.value = Sortable.create(gameItemListEl.value, {
|
||||
animation: 160,
|
||||
@@ -73,11 +73,11 @@ export function useAdminGameManager({
|
||||
chosenClass: 'chosen',
|
||||
onEnd: (evt) => {
|
||||
if (evt.oldIndex == null || evt.newIndex == null || evt.oldIndex === evt.newIndex) return
|
||||
const nextItems = [...(selectedGame.value?.items || [])]
|
||||
const nextItems = [...(selectedTemplate.value?.items || [])]
|
||||
const [moved] = nextItems.splice(evt.oldIndex, 1)
|
||||
nextItems.splice(evt.newIndex, 0, moved)
|
||||
selectedGame.value = {
|
||||
...selectedGame.value,
|
||||
selectedTemplate.value = {
|
||||
...selectedTemplate.value,
|
||||
items: nextItems,
|
||||
}
|
||||
},
|
||||
@@ -87,7 +87,7 @@ export function useAdminGameManager({
|
||||
function mergeRequestItemsIntoDrafts(request) {
|
||||
const requestId = request?.id
|
||||
if (!requestId) return
|
||||
const existingGameSrcs = new Set((selectedGame.value?.items || []).map((item) => normalizeDraftSrc(item?.src)).filter(Boolean))
|
||||
const existingTemplateSrcs = new Set((selectedTemplate.value?.items || []).map((item) => normalizeDraftSrc(item?.src)).filter(Boolean))
|
||||
const existingKeys = new Set(uploadItemDrafts.value.map((draft) => `${draft.kind}:${draft.requestId || ''}:${draft.itemId || draft.file?.name || ''}`))
|
||||
const nextRequestDrafts = (request.items || [])
|
||||
.filter((item) => item?.src)
|
||||
@@ -100,7 +100,7 @@ export function useAdminGameManager({
|
||||
sourceName: requestItemFilename(item),
|
||||
src: item.src,
|
||||
}))
|
||||
.filter((draft) => !existingGameSrcs.has(normalizeDraftSrc(draft.src)))
|
||||
.filter((draft) => !existingTemplateSrcs.has(normalizeDraftSrc(draft.src)))
|
||||
.filter((draft) => !existingKeys.has(`${draft.kind}:${draft.requestId}:${draft.itemId}`))
|
||||
|
||||
if (nextRequestDrafts.length) {
|
||||
@@ -117,13 +117,13 @@ export function useAdminGameManager({
|
||||
uploadFiles.value = uploadItemDrafts.value.filter((draft) => draft.kind === 'file').map((draft) => draft.file).filter(Boolean)
|
||||
}
|
||||
|
||||
async function loadGame(options = {}) {
|
||||
async function loadTemplate(options = {}) {
|
||||
const preserveUploadState = !!options.preserveUploadState
|
||||
resetMessages()
|
||||
if (!preserveUploadState) resetUploadState()
|
||||
|
||||
if (!selectedGameId.value) {
|
||||
selectedGame.value = null
|
||||
if (!selectedTemplateId.value) {
|
||||
selectedTemplate.value = null
|
||||
savedGameItemOrderIds.value = []
|
||||
destroyGameItemSortable()
|
||||
return
|
||||
@@ -131,8 +131,8 @@ export function useAdminGameManager({
|
||||
|
||||
try {
|
||||
isGameLoading.value = true
|
||||
const data = await api.getGame(selectedGameId.value)
|
||||
selectedGame.value = {
|
||||
const data = await api.getGame(selectedTemplateId.value)
|
||||
selectedTemplate.value = {
|
||||
...data,
|
||||
items: (data.items || []).map((item) => ({
|
||||
...item,
|
||||
@@ -142,16 +142,16 @@ export function useAdminGameManager({
|
||||
savedGameItemOrderIds.value = (data.items || []).map((item) => item.id)
|
||||
await syncGameItemSortable()
|
||||
} catch (e) {
|
||||
selectedGame.value = null
|
||||
selectedTemplate.value = null
|
||||
error.value = '템플릿 정보를 불러오지 못했어요.'
|
||||
} finally {
|
||||
isGameLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function createGame(options = {}) {
|
||||
const nextGameId = typeof options.gameId === 'string' ? options.gameId.trim() : newGameId.value.trim()
|
||||
const nextGameName = typeof options.gameName === 'string' ? options.gameName.trim() : newGameName.value.trim()
|
||||
async function createTemplate(options = {}) {
|
||||
const nextGameId = typeof options.gameId === 'string' ? options.gameId.trim() : newTemplateId.value.trim()
|
||||
const nextGameName = typeof options.gameName === 'string' ? options.gameName.trim() : newTemplateName.value.trim()
|
||||
const preserveUploadState = !!options.preserveUploadState
|
||||
resetMessages()
|
||||
try {
|
||||
@@ -162,7 +162,7 @@ export function useAdminGameManager({
|
||||
body: JSON.stringify({
|
||||
id: nextGameId,
|
||||
name: nextGameName,
|
||||
isPublic: !!newGameIsPublic.value,
|
||||
isPublic: !!newTemplateIsPublic.value,
|
||||
thumbnailSrc: activeTemplateRequest.value?.type === 'create' ? (activeTemplateRequest.value?.thumbnailSrc || '') : '',
|
||||
}),
|
||||
})
|
||||
@@ -187,11 +187,11 @@ export function useAdminGameManager({
|
||||
})
|
||||
}
|
||||
}
|
||||
await refreshGames()
|
||||
selectedGameId.value = data.game.id
|
||||
if (customItemModalOpen.value) customItemModalTargetGameId.value = data.game.id
|
||||
closeGameCreateModal()
|
||||
await loadGame({ preserveUploadState })
|
||||
await refreshTemplates()
|
||||
selectedTemplateId.value = data.game.id
|
||||
if (customItemModalOpen.value) customItemModalTargetTemplateId.value = data.game.id
|
||||
closeTemplateCreateModal()
|
||||
await loadTemplate({ preserveUploadState })
|
||||
if (!preserveUploadState && activeTemplateRequest.value?.id) {
|
||||
const sourceRequest = templateRequests.value.find((entry) => entry.id === activeTemplateRequest.value.id) || activeTemplateRequest.value
|
||||
mergeRequestItemsIntoDrafts(sourceRequest)
|
||||
@@ -254,21 +254,21 @@ export function useAdminGameManager({
|
||||
}
|
||||
|
||||
try {
|
||||
if (!selectedGameId.value && activeTemplateRequest.value?.type === 'create' && !activeTemplateRequest.value?.targetGameId) {
|
||||
if (!selectedTemplateId.value && activeTemplateRequest.value?.type === 'create' && !activeTemplateRequest.value?.targetGameId) {
|
||||
const draftGameId = (activeTemplateRequest.value?.draftGameId || '').trim()
|
||||
const draftGameName = (activeTemplateRequest.value?.draftGameName || '').trim()
|
||||
if (!draftGameId || !draftGameName) {
|
||||
error.value = '먼저 신규 템플릿의 이름과 템플릿 ID를 저장해주세요.'
|
||||
return
|
||||
}
|
||||
await createGame({
|
||||
await createTemplate({
|
||||
gameId: draftGameId,
|
||||
gameName: draftGameName,
|
||||
preserveUploadState: true,
|
||||
})
|
||||
}
|
||||
|
||||
if (!selectedGameId.value) {
|
||||
if (!selectedTemplateId.value) {
|
||||
error.value = '템플릿을 먼저 선택해주세요.'
|
||||
return
|
||||
}
|
||||
@@ -283,7 +283,7 @@ export function useAdminGameManager({
|
||||
fd.append('images', entry.file)
|
||||
fd.append('labels', entry.label.trim())
|
||||
})
|
||||
const res = await fetch(toApiUrl(`/api/admin/games/${encodeURIComponent(selectedGameId.value)}/images`), {
|
||||
const res = await fetch(toApiUrl(`/api/admin/games/${encodeURIComponent(selectedTemplateId.value)}/images`), {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
body: fd,
|
||||
@@ -297,7 +297,7 @@ export function useAdminGameManager({
|
||||
for (const requestId of requestIds) {
|
||||
const draftsForRequest = requestDrafts.filter((entry) => entry.requestId === requestId)
|
||||
const result = await api.promoteAdminTemplateRequestItems(requestId, {
|
||||
gameId: selectedGameId.value,
|
||||
gameId: selectedTemplateId.value,
|
||||
itemIds: draftsForRequest.map((entry) => entry.itemId).filter(Boolean),
|
||||
itemSrcs: draftsForRequest.map((entry) => entry.src).filter(Boolean),
|
||||
itemLabels: draftsForRequest.reduce((acc, entry) => {
|
||||
@@ -310,7 +310,7 @@ export function useAdminGameManager({
|
||||
}
|
||||
|
||||
resetUploadState()
|
||||
await loadGame()
|
||||
await loadTemplate()
|
||||
success.value = `템플릿 기본 아이템 ${uploadCount}개 추가를 완료했어요.`
|
||||
} catch (e) {
|
||||
const apiError = e?.data?.error || ''
|
||||
@@ -331,16 +331,16 @@ export function useAdminGameManager({
|
||||
}
|
||||
}
|
||||
|
||||
async function saveGameItemOrder() {
|
||||
async function saveTemplateItemOrder() {
|
||||
resetMessages()
|
||||
if (!selectedGameId.value || !selectedGame.value?.items?.length) return
|
||||
if (!selectedTemplateId.value || !selectedTemplate.value?.items?.length) return
|
||||
|
||||
try {
|
||||
const data = await api.updateAdminGameItemDisplayOrder(selectedGameId.value, {
|
||||
itemIds: selectedGame.value.items.map((item) => item.id),
|
||||
const data = await api.updateAdminGameItemDisplayOrder(selectedTemplateId.value, {
|
||||
itemIds: selectedTemplate.value.items.map((item) => item.id),
|
||||
})
|
||||
selectedGame.value = {
|
||||
...selectedGame.value,
|
||||
selectedTemplate.value = {
|
||||
...selectedTemplate.value,
|
||||
items: (data.items || []).map((item) => ({
|
||||
...item,
|
||||
draftLabel: item.label,
|
||||
@@ -360,13 +360,13 @@ export function useAdminGameManager({
|
||||
syncGameItemSortable,
|
||||
mergeRequestItemsIntoDrafts,
|
||||
removeUploadDraft,
|
||||
loadGame,
|
||||
createGame,
|
||||
loadTemplate,
|
||||
createTemplate,
|
||||
handleItemFiles,
|
||||
onFile,
|
||||
openItemFilePicker,
|
||||
clearItemFiles,
|
||||
uploadItem,
|
||||
saveGameItemOrder,
|
||||
saveTemplateItemOrder,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@ export function useAdminTemplateRequests({
|
||||
activeTemplateRequest,
|
||||
refreshTemplateRequests,
|
||||
setTab,
|
||||
openGameCreateModal,
|
||||
newGameId,
|
||||
newGameName,
|
||||
selectAdminGame,
|
||||
openTemplateCreateModal,
|
||||
newTemplateId,
|
||||
newTemplateName,
|
||||
selectAdminTemplate,
|
||||
mergeRequestItemsIntoDrafts,
|
||||
resetMessages,
|
||||
success,
|
||||
@@ -65,16 +65,16 @@ export function useAdminTemplateRequests({
|
||||
if (request.type === 'create') {
|
||||
const linkedGameId = syncedRequest.targetGameId || ''
|
||||
if (linkedGameId) {
|
||||
await selectAdminGame(linkedGameId)
|
||||
await selectAdminTemplate(linkedGameId)
|
||||
} else {
|
||||
openGameCreateModal()
|
||||
newGameId.value = (syncedRequest.draftGameId || '').trim()
|
||||
newGameName.value = (syncedRequest.draftGameName || '').trim()
|
||||
openTemplateCreateModal()
|
||||
newTemplateId.value = (syncedRequest.draftGameId || '').trim()
|
||||
newTemplateName.value = (syncedRequest.draftGameName || '').trim()
|
||||
}
|
||||
mergeRequestItemsIntoDrafts(syncedRequest)
|
||||
} else {
|
||||
const nextGameId = syncedRequest.targetGameId || syncedRequest.sourceGameId || ''
|
||||
if (nextGameId) await selectAdminGame(nextGameId)
|
||||
if (nextGameId) await selectAdminTemplate(nextGameId)
|
||||
mergeRequestItemsIntoDrafts(syncedRequest)
|
||||
}
|
||||
success.value = '요청 아이템을 템플릿 관리 화면으로 가져왔어요. 필요한 항목만 골라서 추가해 주세요.'
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user