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