Compare commits
	
		
			57 Commits
		
	
	
		
			9f1675e82d
			...
			test
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					0c4e8b8fcb | ||
| 
						 | 
					ee36120ed3 | ||
| 
						 | 
					b5dbccf515 | ||
| 
						 | 
					379e5b67f3 | ||
| 
						 | 
					fd9ea2f5bb | ||
| 
						 | 
					3c28367be9 | ||
| 
						 | 
					8f0958848d | ||
| 
						 | 
					a4cf43b88a | ||
| 
						 | 
					40c5a6593e | ||
| 
						 | 
					edab727c22 | ||
| 
						 | 
					00b12d0edb | ||
| 
						 | 
					6507b025de | ||
| 
						 | 
					cd86973b60 | ||
| 
						 | 
					1e4dcffc17 | ||
| 
						 | 
					5ee0fe6a60 | ||
| 
						 | 
					199049ab7c | ||
| 
						 | 
					bc8833483a | ||
| 
						 | 
					b94aa54365 | ||
| 
						 | 
					478ef2e7fe | ||
| 
						 | 
					63ebe9708f | ||
| 
						 | 
					071502d869 | ||
| 
						 | 
					806af4aba0 | ||
| 
						 | 
					e09f654aba | ||
| 
						 | 
					30e08c862a | ||
| 
						 | 
					231539fd27 | ||
| 
						 | 
					8f502f6d4d | ||
| 
						 | 
					38161af543 | ||
| 
						 | 
					ba248f7680 | ||
| 
						 | 
					a3e82a81f8 | ||
| 
						 | 
					efca5e445d | ||
| 
						 | 
					7ed23047e9 | ||
| 
						 | 
					bbacab88c5 | ||
| 
						 | 
					062bb4f7b2 | ||
| 
						 | 
					6bd3a62134 | ||
| 
						 | 
					d1f700842f | ||
| 
						 | 
					a9e832bc26 | ||
| 
						 | 
					80b298440b | ||
| 
						 | 
					7f56d0b423 | ||
| 
						 | 
					72b1627f3f | ||
| 
						 | 
					13c85bb2a8 | ||
| 
						 | 
					3783714c75 | ||
| 
						 | 
					49cd5a795b | ||
| 
						 | 
					94a989ea57 | ||
| 
						 | 
					439cc21e57 | ||
| 
						 | 
					dbc46482b1 | ||
| 
						 | 
					3aae253180 | ||
| 
						 | 
					89b2f1f740 | ||
| 
						 | 
					09c6605aed | ||
| 
						 | 
					0aff527266 | ||
| 
						 | 
					cea0887d90 | ||
| 
						 | 
					d3f98ec9cb | ||
| 
						 | 
					d8e75f299b | ||
| 
						 | 
					256f65e370 | ||
| 
						 | 
					7821f766e6 | ||
| 
						 | 
					46f966f324 | ||
| 
						 | 
					35be9832e6 | ||
| 
						 | 
					ba14bd1673 | 
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -218,4 +218,7 @@ $RECYCLE.BIN/
 | 
				
			|||||||
# Windows shortcuts
 | 
					# Windows shortcuts
 | 
				
			||||||
*.lnk
 | 
					*.lnk
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.kiro/
 | 
				
			||||||
 | 
					.junie/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# End of https://www.toptal.com/developers/gitignore/api/webstorm,visualstudiocode,vuejs,macos,windows
 | 
					# End of https://www.toptal.com/developers/gitignore/api/webstorm,visualstudiocode,vuejs,macos,windows
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -24,7 +24,7 @@ async function getCalculateCommunityPost(startDate, endDate, page, size) {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function getSettlementRatio(page) {
 | 
					async function getSettlementRatio(page) {
 | 
				
			||||||
    return Vue.axios.get('/admin/calculate/ratio?page=' + (page - 1) + "&size=20'");
 | 
					    return Vue.axios.get('/admin/calculate/ratio?page=' + (page - 1) + "&size=20");
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function createCreatorSettlementRatio(creatorSettlementRatio) {
 | 
					async function createCreatorSettlementRatio(creatorSettlementRatio) {
 | 
				
			||||||
@@ -57,6 +57,21 @@ async function getCalculateCommunityByCreator(startDate, endDate, page, size) {
 | 
				
			|||||||
    )
 | 
					    )
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function updateCreatorSettlementRatio(creatorSettlementRatio) {
 | 
				
			||||||
 | 
					    const request = {
 | 
				
			||||||
 | 
					        memberId: creatorSettlementRatio.creator_id,
 | 
				
			||||||
 | 
					        subsidy: creatorSettlementRatio.subsidy,
 | 
				
			||||||
 | 
					        liveSettlementRatio: creatorSettlementRatio.liveSettlementRatio,
 | 
				
			||||||
 | 
					        contentSettlementRatio: creatorSettlementRatio.contentSettlementRatio,
 | 
				
			||||||
 | 
					        communitySettlementRatio: creatorSettlementRatio.communitySettlementRatio
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    return Vue.axios.post('/admin/calculate/ratio/update', request);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function deleteCreatorSettlementRatio(memberId) {
 | 
				
			||||||
 | 
					    return Vue.axios.post('/admin/calculate/ratio/delete/' + memberId);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export {
 | 
					export {
 | 
				
			||||||
    getCalculateLive,
 | 
					    getCalculateLive,
 | 
				
			||||||
    getCalculateContent,
 | 
					    getCalculateContent,
 | 
				
			||||||
@@ -65,6 +80,8 @@ export {
 | 
				
			|||||||
    getCalculateCommunityPost,
 | 
					    getCalculateCommunityPost,
 | 
				
			||||||
    getSettlementRatio,
 | 
					    getSettlementRatio,
 | 
				
			||||||
    createCreatorSettlementRatio,
 | 
					    createCreatorSettlementRatio,
 | 
				
			||||||
 | 
					    updateCreatorSettlementRatio,
 | 
				
			||||||
 | 
					    deleteCreatorSettlementRatio,
 | 
				
			||||||
    getCalculateLiveByCreator,
 | 
					    getCalculateLiveByCreator,
 | 
				
			||||||
    getCalculateContentByCreator,
 | 
					    getCalculateContentByCreator,
 | 
				
			||||||
    getCalculateCommunityByCreator
 | 
					    getCalculateCommunityByCreator
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,11 +5,11 @@ async function deleteCan(id) {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function getCans() {
 | 
					async function getCans() {
 | 
				
			||||||
    return Vue.axios.get('/can');
 | 
					    return Vue.axios.get('/admin/can');
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function insertCan(can, rewardCan, price) {
 | 
					async function insertCan(can, rewardCan, price, currency) {
 | 
				
			||||||
    const request = {can: can, rewardCan: rewardCan, price: price}
 | 
					    const request = {can: can, rewardCan: rewardCan, price: price, currency}
 | 
				
			||||||
    return Vue.axios.post('/admin/can', request);
 | 
					    return Vue.axios.post('/admin/can', request);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -22,8 +22,8 @@ async function getCouponList(page) {
 | 
				
			|||||||
    return Vue.axios.get('/can/coupon?page=' + (page - 1) + "&size=20");
 | 
					    return Vue.axios.get('/can/coupon?page=' + (page - 1) + "&size=20");
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function generateCoupon(couponName, can, validity, isMultipleUse, couponNumberCount) {
 | 
					async function generateCoupon(couponName, couponType, can, validity, isMultipleUse, couponNumberCount) {
 | 
				
			||||||
    const request = {couponName, can, validity: validity + ' 23:59:59', isMultipleUse, couponNumberCount};
 | 
					    const request = {couponName, couponType, can, validity: validity + ' 23:59:59', isMultipleUse, couponNumberCount};
 | 
				
			||||||
    return Vue.axios.post('/can/coupon', request);
 | 
					    return Vue.axios.post('/can/coupon', request);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										293
									
								
								src/api/character.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										293
									
								
								src/api/character.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,293 @@
 | 
				
			|||||||
 | 
					import Vue from 'vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 캐릭터 리스트
 | 
				
			||||||
 | 
					async function getCharacterList(page = 1, size = 20) {
 | 
				
			||||||
 | 
					  return Vue.axios.get('/admin/chat/character/list', {
 | 
				
			||||||
 | 
					    params: { page: page - 1, size }
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 캐릭터 검색 (배너용 기존 함수)
 | 
				
			||||||
 | 
					async function searchCharacters(searchTerm, page = 1, size = 20) {
 | 
				
			||||||
 | 
					  return Vue.axios.get('/admin/chat/banner/search-character', {
 | 
				
			||||||
 | 
					    params: { searchTerm, page: page - 1, size }
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 캐릭터 리스트 검색 (요구사항: /admin/chat/character/search)
 | 
				
			||||||
 | 
					async function searchCharacterList(searchTerm, page = 1, size = 20) {
 | 
				
			||||||
 | 
					  return Vue.axios.get('/admin/chat/character/search', {
 | 
				
			||||||
 | 
					    params: { searchTerm, page: page - 1, size }
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 캐릭터 상세 조회
 | 
				
			||||||
 | 
					async function getCharacter(id) {
 | 
				
			||||||
 | 
					  return Vue.axios.get(`/admin/chat/character/${id}`)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 내부 헬퍼: 빈 문자열을 null로 변환
 | 
				
			||||||
 | 
					function toNullIfBlank(value) {
 | 
				
			||||||
 | 
					  if (typeof value === 'string') {
 | 
				
			||||||
 | 
					    return value.trim() === '' ? null : value;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return value === '' ? null : value;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 캐릭터 등록
 | 
				
			||||||
 | 
					async function createCharacter(characterData) {
 | 
				
			||||||
 | 
					  const formData = new FormData()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 이미지만 FormData에 추가
 | 
				
			||||||
 | 
					  if (characterData.image) formData.append('image', characterData.image)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 나머지 데이터는 JSON 문자열로 변환하여 request 필드에 추가
 | 
				
			||||||
 | 
					  const requestData = {
 | 
				
			||||||
 | 
					    name: toNullIfBlank(characterData.name),
 | 
				
			||||||
 | 
					    systemPrompt: toNullIfBlank(characterData.systemPrompt),
 | 
				
			||||||
 | 
					    description: toNullIfBlank(characterData.description),
 | 
				
			||||||
 | 
					    age: toNullIfBlank(characterData.age),
 | 
				
			||||||
 | 
					    gender: toNullIfBlank(characterData.gender),
 | 
				
			||||||
 | 
					    mbti: toNullIfBlank(characterData.mbti),
 | 
				
			||||||
 | 
					    characterType: toNullIfBlank(characterData.type),
 | 
				
			||||||
 | 
					    originalWorkId: characterData.originalWorkId || null,
 | 
				
			||||||
 | 
					    speechPattern: toNullIfBlank(characterData.speechPattern),
 | 
				
			||||||
 | 
					    speechStyle: toNullIfBlank(characterData.speechStyle),
 | 
				
			||||||
 | 
					    appearance: toNullIfBlank(characterData.appearance),
 | 
				
			||||||
 | 
					    tags: characterData.tags || [],
 | 
				
			||||||
 | 
					    hobbies: characterData.hobbies || [],
 | 
				
			||||||
 | 
					    values: characterData.values || [],
 | 
				
			||||||
 | 
					    goals: characterData.goals || [],
 | 
				
			||||||
 | 
					    relationships: characterData.relationships || [],
 | 
				
			||||||
 | 
					    personalities: characterData.personalities || [],
 | 
				
			||||||
 | 
					    backgrounds: characterData.backgrounds || [],
 | 
				
			||||||
 | 
					    memories: characterData.memories || []
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  formData.append('request', JSON.stringify(requestData))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return Vue.axios.post('/admin/chat/character/register', formData, {
 | 
				
			||||||
 | 
					    headers: {
 | 
				
			||||||
 | 
					      'Content-Type': 'multipart/form-data'
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 캐릭터 수정
 | 
				
			||||||
 | 
					async function updateCharacter(characterData, image = null) {
 | 
				
			||||||
 | 
					  const formData = new FormData()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 이미지가 있는 경우에만 FormData에 추가
 | 
				
			||||||
 | 
					  if (image) formData.append('image', image)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 변경된 데이터만 JSON 문자열로 변환하여 request 필드에 추가 ('' -> null 변환)
 | 
				
			||||||
 | 
					  // characterData는 이미 변경된 필드만 포함하고 있음
 | 
				
			||||||
 | 
					  const processed = {}
 | 
				
			||||||
 | 
					  Object.keys(characterData).forEach(key => {
 | 
				
			||||||
 | 
					    const value = characterData[key]
 | 
				
			||||||
 | 
					    if (typeof value === 'string' || value === '') {
 | 
				
			||||||
 | 
					      processed[key] = toNullIfBlank(value)
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      processed[key] = value
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  formData.append('request', JSON.stringify(processed))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return Vue.axios.put(`/admin/chat/character/update`, formData, {
 | 
				
			||||||
 | 
					    headers: {
 | 
				
			||||||
 | 
					      'Content-Type': 'multipart/form-data'
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 캐릭터 배너 리스트 조회
 | 
				
			||||||
 | 
					async function getCharacterBannerList(page = 1, size = 20) {
 | 
				
			||||||
 | 
					  return Vue.axios.get('/admin/chat/banner/list', {
 | 
				
			||||||
 | 
					    params: { page: page - 1, size }
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 캐릭터 배너 등록
 | 
				
			||||||
 | 
					async function createCharacterBanner(bannerData) {
 | 
				
			||||||
 | 
					  const formData = new FormData()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 이미지 FormData에 추가
 | 
				
			||||||
 | 
					  if (bannerData.image) formData.append('image', bannerData.image)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 캐릭터 ID를 JSON 문자열로 변환하여 request 필드에 추가
 | 
				
			||||||
 | 
					  const requestData = {
 | 
				
			||||||
 | 
					    characterId: bannerData.characterId
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  formData.append('request', JSON.stringify(requestData))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return Vue.axios.post('/admin/chat/banner/register', formData, {
 | 
				
			||||||
 | 
					    headers: {
 | 
				
			||||||
 | 
					      'Content-Type': 'multipart/form-data'
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 캐릭터 배너 수정
 | 
				
			||||||
 | 
					async function updateCharacterBanner(bannerData) {
 | 
				
			||||||
 | 
					  const formData = new FormData()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 이미지가 있는 경우에만 FormData에 추가
 | 
				
			||||||
 | 
					  if (bannerData.image) formData.append('image', bannerData.image)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 캐릭터 ID와 배너 ID를 JSON 문자열로 변환하여 request 필드에 추가
 | 
				
			||||||
 | 
					  const requestData = {
 | 
				
			||||||
 | 
					    characterId: bannerData.characterId,
 | 
				
			||||||
 | 
					    bannerId: bannerData.bannerId
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  formData.append('request', JSON.stringify(requestData))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return Vue.axios.put('/admin/chat/banner/update', formData, {
 | 
				
			||||||
 | 
					    headers: {
 | 
				
			||||||
 | 
					      'Content-Type': 'multipart/form-data'
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 캐릭터 배너 삭제
 | 
				
			||||||
 | 
					async function deleteCharacterBanner(bannerId) {
 | 
				
			||||||
 | 
					  return Vue.axios.delete(`/admin/chat/banner/${bannerId}`)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 캐릭터 배너 순서 변경
 | 
				
			||||||
 | 
					async function updateCharacterBannerOrder(bannerIds) {
 | 
				
			||||||
 | 
					  return Vue.axios.put('/admin/chat/banner/orders', {ids: bannerIds})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 캐릭터 이미지 리스트
 | 
				
			||||||
 | 
					async function getCharacterImageList(characterId, page = 1, size = 20) {
 | 
				
			||||||
 | 
					  return Vue.axios.get('/admin/chat/character/image/list', {
 | 
				
			||||||
 | 
					    params: { characterId, page: page - 1, size }
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 캐릭터 이미지 상세
 | 
				
			||||||
 | 
					async function getCharacterImage(imageId) {
 | 
				
			||||||
 | 
					  return Vue.axios.get(`/admin/chat/character/image/${imageId}`)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 캐릭터 이미지 등록
 | 
				
			||||||
 | 
					async function createCharacterImage(imageData) {
 | 
				
			||||||
 | 
					  const formData = new FormData()
 | 
				
			||||||
 | 
					  if (imageData.image) formData.append('image', imageData.image)
 | 
				
			||||||
 | 
					  const requestData = {
 | 
				
			||||||
 | 
					    characterId: imageData.characterId,
 | 
				
			||||||
 | 
					    imagePriceCan: imageData.imagePriceCan,
 | 
				
			||||||
 | 
					    messagePriceCan: imageData.messagePriceCan,
 | 
				
			||||||
 | 
					    isAdult: imageData.isAdult,
 | 
				
			||||||
 | 
					    triggers: imageData.triggers || []
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  formData.append('request', JSON.stringify(requestData))
 | 
				
			||||||
 | 
					  return Vue.axios.post('/admin/chat/character/image/register', formData, {
 | 
				
			||||||
 | 
					    headers: { 'Content-Type': 'multipart/form-data' }
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 캐릭터 이미지 수정 (트리거만 수정)
 | 
				
			||||||
 | 
					async function updateCharacterImage(imageData) {
 | 
				
			||||||
 | 
					  const imageId = imageData.imageId
 | 
				
			||||||
 | 
					  const payload = { triggers: imageData.triggers || [] }
 | 
				
			||||||
 | 
					  return Vue.axios.put(`/admin/chat/character/image/${imageId}/triggers`, payload)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 캐릭터 이미지 삭제
 | 
				
			||||||
 | 
					async function deleteCharacterImage(imageId) {
 | 
				
			||||||
 | 
					  return Vue.axios.delete(`/admin/chat/character/image/${imageId}`)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 캐릭터 이미지 순서 변경
 | 
				
			||||||
 | 
					async function updateCharacterImageOrder(characterId, imageIds) {
 | 
				
			||||||
 | 
					  return Vue.axios.put('/admin/chat/character/image/orders', { characterId, ids: imageIds })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 캐릭터 큐레이션 목록
 | 
				
			||||||
 | 
					async function getCharacterCurationList() {
 | 
				
			||||||
 | 
					  return Vue.axios.get('/admin/chat/character/curation/list')
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 캐릭터 큐레이션 등록
 | 
				
			||||||
 | 
					async function createCharacterCuration({ title, isAdult, isActive }) {
 | 
				
			||||||
 | 
					  return Vue.axios.post('/admin/chat/character/curation/register', { title, isAdult, isActive })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 캐릭터 큐레이션 수정
 | 
				
			||||||
 | 
					// payload: { id: Long, title?, isAdult?, isActive? }
 | 
				
			||||||
 | 
					async function updateCharacterCuration(payload) {
 | 
				
			||||||
 | 
					  return Vue.axios.put('/admin/chat/character/curation/update', payload)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 캐릭터 큐레이션 삭제
 | 
				
			||||||
 | 
					async function deleteCharacterCuration(curationId) {
 | 
				
			||||||
 | 
					  return Vue.axios.delete(`/admin/chat/character/curation/${curationId}`)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 캐릭터 큐레이션 정렬 순서 변경
 | 
				
			||||||
 | 
					async function updateCharacterCurationOrder(ids) {
 | 
				
			||||||
 | 
					  return Vue.axios.put('/admin/chat/character/curation/reorder', { ids })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 큐레이션에 캐릭터 등록 (다중 등록)
 | 
				
			||||||
 | 
					// characterIds: Array<Long>
 | 
				
			||||||
 | 
					async function addCharacterToCuration(curationId, characterIds) {
 | 
				
			||||||
 | 
					  return Vue.axios.post(`/admin/chat/character/curation/${curationId}/characters`, { characterIds })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 큐레이션에서 캐릭터 삭제
 | 
				
			||||||
 | 
					async function removeCharacterFromCuration(curationId, characterId) {
 | 
				
			||||||
 | 
					  return Vue.axios.delete(`/admin/chat/character/curation/${curationId}/characters/${characterId}`)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 큐레이션 내 캐릭터 정렬 순서 변경
 | 
				
			||||||
 | 
					async function updateCurationCharactersOrder(curationId, characterIds) {
 | 
				
			||||||
 | 
					  return Vue.axios.put(`/admin/chat/character/curation/${curationId}/characters/reorder`, { characterIds })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 큐레이션 캐릭터 목록 조회 (가정된 엔드포인트)
 | 
				
			||||||
 | 
					async function getCharactersInCuration(curationId) {
 | 
				
			||||||
 | 
					  return Vue.axios.get(`/admin/chat/character/curation/${curationId}/characters`)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 캐릭터별 정산 목록
 | 
				
			||||||
 | 
					// params: { startDateStr, endDateStr, sort, page, size }
 | 
				
			||||||
 | 
					async function getCharacterCalculateList({ startDateStr, endDateStr, sort = 'TOTAL_SALES_DESC', page = 0, size = 30 }) {
 | 
				
			||||||
 | 
					  return Vue.axios.get('/admin/chat/calculate/characters', {
 | 
				
			||||||
 | 
					    params: { startDateStr, endDateStr, sort, page, size }
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export {
 | 
				
			||||||
 | 
					  getCharacterList,
 | 
				
			||||||
 | 
					  searchCharacters,
 | 
				
			||||||
 | 
					  searchCharacterList,
 | 
				
			||||||
 | 
					  getCharacter,
 | 
				
			||||||
 | 
					  createCharacter,
 | 
				
			||||||
 | 
					  updateCharacter,
 | 
				
			||||||
 | 
					  getCharacterBannerList,
 | 
				
			||||||
 | 
					  createCharacterBanner,
 | 
				
			||||||
 | 
					  updateCharacterBanner,
 | 
				
			||||||
 | 
					  deleteCharacterBanner,
 | 
				
			||||||
 | 
					  updateCharacterBannerOrder,
 | 
				
			||||||
 | 
					  getCharacterImageList,
 | 
				
			||||||
 | 
					  getCharacterImage,
 | 
				
			||||||
 | 
					  createCharacterImage,
 | 
				
			||||||
 | 
					  updateCharacterImage,
 | 
				
			||||||
 | 
					  deleteCharacterImage,
 | 
				
			||||||
 | 
					  updateCharacterImageOrder,
 | 
				
			||||||
 | 
					  // Character Curation
 | 
				
			||||||
 | 
					  getCharacterCurationList,
 | 
				
			||||||
 | 
					  createCharacterCuration,
 | 
				
			||||||
 | 
					  updateCharacterCuration,
 | 
				
			||||||
 | 
					  deleteCharacterCuration,
 | 
				
			||||||
 | 
					  updateCharacterCurationOrder,
 | 
				
			||||||
 | 
					  addCharacterToCuration,
 | 
				
			||||||
 | 
					  removeCharacterFromCuration,
 | 
				
			||||||
 | 
					  updateCurationCharactersOrder,
 | 
				
			||||||
 | 
					  getCharactersInCuration,
 | 
				
			||||||
 | 
					  getCharacterCalculateList
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -4,8 +4,11 @@ async function getChargeStatus(startDate, endDate) {
 | 
				
			|||||||
    return Vue.axios.get('/admin/charge/status?startDateStr=' + startDate + '&endDateStr=' + endDate);
 | 
					    return Vue.axios.get('/admin/charge/status?startDateStr=' + startDate + '&endDateStr=' + endDate);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function getChargeStatusDetail(startDate, paymentGateway) {
 | 
					async function getChargeStatusDetail(startDate, paymentGateway, currency) {
 | 
				
			||||||
    return Vue.axios.get('/admin/charge/status/detail?startDateStr=' + startDate + '&paymentGateway=' + paymentGateway);
 | 
					    return Vue.axios.get('/admin/charge/status/detail?startDateStr=' + startDate
 | 
				
			||||||
 | 
					      + '&paymentGateway=' + paymentGateway
 | 
				
			||||||
 | 
					      + '¤cy=' + currency
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export { getChargeStatus, getChargeStatusDetail }
 | 
					export { getChargeStatus, getChargeStatusDetail }
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										87
									
								
								src/api/original.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								src/api/original.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,87 @@
 | 
				
			|||||||
 | 
					import Vue from 'vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 공통: 값 그대로 전달 (빈 문자열 유지)
 | 
				
			||||||
 | 
					function toNullIfBlank(value) {
 | 
				
			||||||
 | 
					  if (typeof value === 'string') {
 | 
				
			||||||
 | 
					    return value.trim() === '' ? null : value;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return value === '' ? null : value;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 원작 리스트
 | 
				
			||||||
 | 
					export async function getOriginalList(page = 1, size = 20) {
 | 
				
			||||||
 | 
					  return Vue.axios.get('/admin/chat/original/list', {
 | 
				
			||||||
 | 
					    params: { page: page - 1, size }
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 원작 등록
 | 
				
			||||||
 | 
					export async function createOriginal(data) {
 | 
				
			||||||
 | 
					  const formData = new FormData();
 | 
				
			||||||
 | 
					  if (data.image) formData.append('image', data.image);
 | 
				
			||||||
 | 
					  const request = {
 | 
				
			||||||
 | 
					    title: toNullIfBlank(data.title),
 | 
				
			||||||
 | 
					    contentType: toNullIfBlank(data.contentType),
 | 
				
			||||||
 | 
					    category: toNullIfBlank(data.category),
 | 
				
			||||||
 | 
					    isAdult: !!data.isAdult,
 | 
				
			||||||
 | 
					    description: toNullIfBlank(data.description),
 | 
				
			||||||
 | 
					    originalLink: toNullIfBlank(data.originalLink), // 원천 원작 링크
 | 
				
			||||||
 | 
					    originalWork: toNullIfBlank(data.originalWork),
 | 
				
			||||||
 | 
					    writer: toNullIfBlank(data.writer),
 | 
				
			||||||
 | 
					    studio: toNullIfBlank(data.studio),
 | 
				
			||||||
 | 
					    originalLinks: Array.isArray(data.originalLinks) ? data.originalLinks : [],
 | 
				
			||||||
 | 
					    tags: Array.isArray(data.tags) ? data.tags : []
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  formData.append('request', JSON.stringify(request));
 | 
				
			||||||
 | 
					  return Vue.axios.post('/admin/chat/original/register', formData, {
 | 
				
			||||||
 | 
					    headers: { 'Content-Type': 'multipart/form-data' }
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 원작 수정
 | 
				
			||||||
 | 
					export async function updateOriginal(data, image = null) {
 | 
				
			||||||
 | 
					  const formData = new FormData();
 | 
				
			||||||
 | 
					  if (image) formData.append('image', image);
 | 
				
			||||||
 | 
					  const processed = {};
 | 
				
			||||||
 | 
					  Object.keys(data).forEach(key => {
 | 
				
			||||||
 | 
					    processed[key] = data[key];
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  formData.append('request', JSON.stringify(processed));
 | 
				
			||||||
 | 
					  return Vue.axios.put('/admin/chat/original/update', formData, {
 | 
				
			||||||
 | 
					    headers: { 'Content-Type': 'multipart/form-data' }
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 원작 삭제
 | 
				
			||||||
 | 
					export async function deleteOriginal(id) {
 | 
				
			||||||
 | 
					  return Vue.axios.delete(`/admin/chat/original/${id}`)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 원작 상세
 | 
				
			||||||
 | 
					export async function getOriginal(id) {
 | 
				
			||||||
 | 
					  return Vue.axios.get(`/admin/chat/original/${id}`)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 원작 속 캐릭터 조회
 | 
				
			||||||
 | 
					export async function getOriginalCharacters(id, page = 1, size = 20) {
 | 
				
			||||||
 | 
					  return Vue.axios.get(`/admin/chat/original/${id}/characters`, {
 | 
				
			||||||
 | 
					    params: { page: page - 1, size }
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 원작 검색
 | 
				
			||||||
 | 
					export async function searchOriginals(searchTerm) {
 | 
				
			||||||
 | 
					  return Vue.axios.get('/admin/chat/original/search', {
 | 
				
			||||||
 | 
					    params: { searchTerm }
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 원작에 캐릭터 연결
 | 
				
			||||||
 | 
					export async function assignCharactersToOriginal(id, characterIds = []) {
 | 
				
			||||||
 | 
					  return Vue.axios.post(`/admin/chat/original/${id}/assign-characters`, { characterIds })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 원작에서 캐릭터 연결 해제
 | 
				
			||||||
 | 
					export async function unassignCharactersFromOriginal(id, characterIds = []) {
 | 
				
			||||||
 | 
					  return Vue.axios.post(`/admin/chat/original/${id}/unassign-characters`, { characterIds })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										19
									
								
								src/api/point_policy.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/api/point_policy.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					import Vue from 'vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function getPointPolicyList(page) {
 | 
				
			||||||
 | 
					    return Vue.axios.get("/admin/point-policies?page=" + (page - 1) + "&page_size=20")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function createPointPolicyList(request) {
 | 
				
			||||||
 | 
					    return Vue.axios.post("/admin/point-policies", request)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function updatePointPolicyList(id, request) {
 | 
				
			||||||
 | 
					    return Vue.axios.put("/admin/point-policies/" + id, request)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export {
 | 
				
			||||||
 | 
					    getPointPolicyList,
 | 
				
			||||||
 | 
					    createPointPolicyList,
 | 
				
			||||||
 | 
					    updatePointPolicyList
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -43,6 +43,7 @@
 | 
				
			|||||||
          >
 | 
					          >
 | 
				
			||||||
            <v-list-item
 | 
					            <v-list-item
 | 
				
			||||||
              :to="childItem.route"
 | 
					              :to="childItem.route"
 | 
				
			||||||
 | 
					              :exact="childItem.route === '/character'"
 | 
				
			||||||
              active-class="blue white--text"
 | 
					              active-class="blue white--text"
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
              <v-list-item-title>{{ childItem.title }}</v-list-item-title>
 | 
					              <v-list-item-title>{{ childItem.title }}</v-list-item-title>
 | 
				
			||||||
@@ -95,6 +96,39 @@ export default {
 | 
				
			|||||||
        let res = await api.getMenus();
 | 
					        let res = await api.getMenus();
 | 
				
			||||||
        if (res.status === 200 && res.data.success === true && res.data.data.length > 0) {
 | 
					        if (res.status === 200 && res.data.success === true && res.data.data.length > 0) {
 | 
				
			||||||
          this.items = res.data.data
 | 
					          this.items = res.data.data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          // 캐릭터 챗봇 메뉴 추가
 | 
				
			||||||
 | 
					          this.items.push({
 | 
				
			||||||
 | 
					            title: '캐릭터 챗봇',
 | 
				
			||||||
 | 
					            route: null,
 | 
				
			||||||
 | 
					            items: [
 | 
				
			||||||
 | 
					              {
 | 
				
			||||||
 | 
					                title: '배너 등록',
 | 
				
			||||||
 | 
					                route: '/character/banner',
 | 
				
			||||||
 | 
					                items: null
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					              {
 | 
				
			||||||
 | 
					                title: '캐릭터 리스트',
 | 
				
			||||||
 | 
					                route: '/character',
 | 
				
			||||||
 | 
					                items: null
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					              {
 | 
				
			||||||
 | 
					                title: '큐레이션',
 | 
				
			||||||
 | 
					                route: '/character/curation',
 | 
				
			||||||
 | 
					                items: null
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					              {
 | 
				
			||||||
 | 
					                title: '정산',
 | 
				
			||||||
 | 
					                route: '/character/calculate',
 | 
				
			||||||
 | 
					                items: null
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					              {
 | 
				
			||||||
 | 
					                title: '원작',
 | 
				
			||||||
 | 
					                route: '/original-work',
 | 
				
			||||||
 | 
					                items: null
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					            ]
 | 
				
			||||||
 | 
					          })
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
          this.notifyError("알 수 없는 오류가 발생했습니다. 다시 로그인 해주세요!")
 | 
					          this.notifyError("알 수 없는 오류가 발생했습니다. 다시 로그인 해주세요!")
 | 
				
			||||||
          this.logout();
 | 
					          this.logout();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -135,6 +135,11 @@ const routes = [
 | 
				
			|||||||
                name: 'ChargeEvent',
 | 
					                name: 'ChargeEvent',
 | 
				
			||||||
                component: () => import(/* webpackChunkName: "promotion" */ '../views/Promotion/ChargeEvent.vue')
 | 
					                component: () => import(/* webpackChunkName: "promotion" */ '../views/Promotion/ChargeEvent.vue')
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                path: '/promotion/point-policy',
 | 
				
			||||||
 | 
					                name: 'PointPolicyView',
 | 
				
			||||||
 | 
					                component: () => import(/* webpackChunkName: "promotion" */ '../views/Promotion/PointPolicyView.vue')
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                path: '/can/management',
 | 
					                path: '/can/management',
 | 
				
			||||||
                name: 'CoinView',
 | 
					                name: 'CoinView',
 | 
				
			||||||
@@ -250,6 +255,61 @@ const routes = [
 | 
				
			|||||||
                name: 'MarketingAdStatisticsView',
 | 
					                name: 'MarketingAdStatisticsView',
 | 
				
			||||||
                component: () => import(/* webpackChunkName: "marketing" */ '../views/Marketing/MarketingAdStatisticsView.vue')
 | 
					                component: () => import(/* webpackChunkName: "marketing" */ '../views/Marketing/MarketingAdStatisticsView.vue')
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                path: '/character',
 | 
				
			||||||
 | 
					                name: 'CharacterList',
 | 
				
			||||||
 | 
					                component: () => import(/* webpackChunkName: "character" */ '../views/Chat/CharacterList.vue')
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                path: '/character/form',
 | 
				
			||||||
 | 
					                name: 'CharacterForm',
 | 
				
			||||||
 | 
					                component: () => import(/* webpackChunkName: "character" */ '../views/Chat/CharacterForm.vue')
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                path: '/character/banner',
 | 
				
			||||||
 | 
					                name: 'CharacterBanner',
 | 
				
			||||||
 | 
					                component: () => import(/* webpackChunkName: "character" */ '../views/Chat/CharacterBanner.vue')
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                path: '/character/images',
 | 
				
			||||||
 | 
					                name: 'CharacterImageList',
 | 
				
			||||||
 | 
					                component: () => import(/* webpackChunkName: "character" */ '../views/Chat/CharacterImageList.vue')
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                path: '/character/images/form',
 | 
				
			||||||
 | 
					                name: 'CharacterImageForm',
 | 
				
			||||||
 | 
					                component: () => import(/* webpackChunkName: "character" */ '../views/Chat/CharacterImageForm.vue')
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                path: '/character/curation',
 | 
				
			||||||
 | 
					                name: 'CharacterCuration',
 | 
				
			||||||
 | 
					                component: () => import(/* webpackChunkName: "character" */ '../views/Chat/CharacterCuration.vue')
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                path: '/character/curation/detail',
 | 
				
			||||||
 | 
					                name: 'CharacterCurationDetail',
 | 
				
			||||||
 | 
					                component: () => import(/* webpackChunkName: "character" */ '../views/Chat/CharacterCurationDetail.vue')
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                path: '/character/calculate',
 | 
				
			||||||
 | 
					                name: 'CharacterCalculate',
 | 
				
			||||||
 | 
					                component: () => import(/* webpackChunkName: "character" */ '../views/Chat/CharacterCalculateList.vue')
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                path: '/original-work',
 | 
				
			||||||
 | 
					                name: 'OriginalList',
 | 
				
			||||||
 | 
					                component: () => import(/* webpackChunkName: "original" */ '../views/Chat/OriginalList.vue')
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                path: '/original-work/form',
 | 
				
			||||||
 | 
					                name: 'OriginalForm',
 | 
				
			||||||
 | 
					                component: () => import(/* webpackChunkName: "original" */ '../views/Chat/OriginalForm.vue')
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                path: '/original-work/detail',
 | 
				
			||||||
 | 
					                name: 'OriginalDetail',
 | 
				
			||||||
 | 
					                component: () => import(/* webpackChunkName: "original" */ '../views/Chat/OriginalDetail.vue')
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -60,7 +60,7 @@
 | 
				
			|||||||
            <v-card-text>
 | 
					            <v-card-text>
 | 
				
			||||||
              지급할 캔 수: {{ can }} 캔
 | 
					              지급할 캔 수: {{ can }} 캔
 | 
				
			||||||
            </v-card-text>
 | 
					            </v-card-text>
 | 
				
			||||||
            <v-card-actions v-show="!isLoading">
 | 
					            <v-card-actions v-show="!is_loading">
 | 
				
			||||||
              <v-spacer />
 | 
					              <v-spacer />
 | 
				
			||||||
              <v-btn
 | 
					              <v-btn
 | 
				
			||||||
                color="blue darken-1"
 | 
					                color="blue darken-1"
 | 
				
			||||||
@@ -95,7 +95,7 @@ export default {
 | 
				
			|||||||
  data() {
 | 
					  data() {
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      show_confirm: false,
 | 
					      show_confirm: false,
 | 
				
			||||||
      isLoading: false,
 | 
					      is_loading: false,
 | 
				
			||||||
      account_id: '',
 | 
					      account_id: '',
 | 
				
			||||||
      method: '',
 | 
					      method: '',
 | 
				
			||||||
      can: ''
 | 
					      can: ''
 | 
				
			||||||
@@ -124,7 +124,7 @@ export default {
 | 
				
			|||||||
        return this.notifyError('캔은 숫자만 넣을 수 있습니다.')
 | 
					        return this.notifyError('캔은 숫자만 넣을 수 있습니다.')
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (!this.isLoading) {
 | 
					      if (!this.is_loading) {
 | 
				
			||||||
        this.show_confirm = true
 | 
					        this.show_confirm = true
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
@@ -134,8 +134,8 @@ export default {
 | 
				
			|||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async submit() {
 | 
					    async submit() {
 | 
				
			||||||
      if (!this.isLoading) {
 | 
					      if (!this.is_loading) {
 | 
				
			||||||
        this.isLoading = true
 | 
					        this.is_loading = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
          this.show_confirm = false
 | 
					          this.show_confirm = false
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,7 +14,7 @@
 | 
				
			|||||||
        <v-col>
 | 
					        <v-col>
 | 
				
			||||||
          <v-btn
 | 
					          <v-btn
 | 
				
			||||||
            block
 | 
					            block
 | 
				
			||||||
            color="#9970ff"
 | 
					            color="#3bb9f1"
 | 
				
			||||||
            dark
 | 
					            dark
 | 
				
			||||||
            depressed
 | 
					            depressed
 | 
				
			||||||
            @click="showWriteDialog"
 | 
					            @click="showWriteDialog"
 | 
				
			||||||
@@ -42,6 +42,10 @@
 | 
				
			|||||||
              <span @click="getCouponNumberList(item)">{{ item.couponName }}</span>
 | 
					              <span @click="getCouponNumberList(item)">{{ item.couponName }}</span>
 | 
				
			||||||
            </template>
 | 
					            </template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <template v-slot:item.couponType="{ item }">
 | 
				
			||||||
 | 
					              {{ item.couponType }}
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <template v-slot:item.can="{ item }">
 | 
					            <template v-slot:item.can="{ item }">
 | 
				
			||||||
              {{ item.can.toLocaleString('en-US') }}
 | 
					              {{ item.can.toLocaleString('en-US') }}
 | 
				
			||||||
            </template>
 | 
					            </template>
 | 
				
			||||||
@@ -132,6 +136,38 @@
 | 
				
			|||||||
              label="발행수량"
 | 
					              label="발행수량"
 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
          </v-card-text>
 | 
					          </v-card-text>
 | 
				
			||||||
 | 
					          <v-card-text>
 | 
				
			||||||
 | 
					            <v-row align="center">
 | 
				
			||||||
 | 
					              <v-col cols="4">
 | 
				
			||||||
 | 
					                쿠폰종류
 | 
				
			||||||
 | 
					              </v-col>
 | 
				
			||||||
 | 
					              <v-col
 | 
				
			||||||
 | 
					                cols="8"
 | 
				
			||||||
 | 
					                class="datepicker-wrapper"
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <v-row>
 | 
				
			||||||
 | 
					                  <v-col>
 | 
				
			||||||
 | 
					                    <input
 | 
				
			||||||
 | 
					                      id="can_coupon"
 | 
				
			||||||
 | 
					                      v-model="coupon_type"
 | 
				
			||||||
 | 
					                      type="radio"
 | 
				
			||||||
 | 
					                      value="CAN"
 | 
				
			||||||
 | 
					                    >
 | 
				
			||||||
 | 
					                    <label for="can_coupon"> 캔 쿠폰</label>
 | 
				
			||||||
 | 
					                  </v-col>
 | 
				
			||||||
 | 
					                  <v-col>
 | 
				
			||||||
 | 
					                    <input
 | 
				
			||||||
 | 
					                      id="point_coupon"
 | 
				
			||||||
 | 
					                      v-model="coupon_type"
 | 
				
			||||||
 | 
					                      type="radio"
 | 
				
			||||||
 | 
					                      value="POINT"
 | 
				
			||||||
 | 
					                    >
 | 
				
			||||||
 | 
					                    <label for="point_coupon"> 포인트 쿠폰</label>
 | 
				
			||||||
 | 
					                  </v-col>
 | 
				
			||||||
 | 
					                </v-row>
 | 
				
			||||||
 | 
					              </v-col>
 | 
				
			||||||
 | 
					            </v-row>
 | 
				
			||||||
 | 
					          </v-card-text>
 | 
				
			||||||
          <v-card-text>
 | 
					          <v-card-text>
 | 
				
			||||||
            <v-row align="center">
 | 
					            <v-row align="center">
 | 
				
			||||||
              <v-col cols="4">
 | 
					              <v-col cols="4">
 | 
				
			||||||
@@ -373,6 +409,7 @@ export default {
 | 
				
			|||||||
      is_active: null,
 | 
					      is_active: null,
 | 
				
			||||||
      is_multiple_use: false,
 | 
					      is_multiple_use: false,
 | 
				
			||||||
      coupon_number_count: null,
 | 
					      coupon_number_count: null,
 | 
				
			||||||
 | 
					      coupon_type: 'CAN',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      page: 1,
 | 
					      page: 1,
 | 
				
			||||||
      total_page: 0,
 | 
					      total_page: 0,
 | 
				
			||||||
@@ -414,6 +451,12 @@ export default {
 | 
				
			|||||||
          sortable: false,
 | 
					          sortable: false,
 | 
				
			||||||
          value: 'couponName',
 | 
					          value: 'couponName',
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          text: '쿠폰종류',
 | 
				
			||||||
 | 
					          align: 'center',
 | 
				
			||||||
 | 
					          sortable: false,
 | 
				
			||||||
 | 
					          value: 'couponType',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          text: '쿠폰금액',
 | 
					          text: '쿠폰금액',
 | 
				
			||||||
          align: 'center',
 | 
					          align: 'center',
 | 
				
			||||||
@@ -542,6 +585,7 @@ export default {
 | 
				
			|||||||
      this.is_active = null
 | 
					      this.is_active = null
 | 
				
			||||||
      this.is_multiple_use = false
 | 
					      this.is_multiple_use = false
 | 
				
			||||||
      this.coupon_number_count = null
 | 
					      this.coupon_number_count = null
 | 
				
			||||||
 | 
					      this.coupon_type = 'CAN'
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    showModifyDialog(value) {
 | 
					    showModifyDialog(value) {
 | 
				
			||||||
@@ -628,6 +672,7 @@ export default {
 | 
				
			|||||||
      try {
 | 
					      try {
 | 
				
			||||||
        const res = await api.generateCoupon(
 | 
					        const res = await api.generateCoupon(
 | 
				
			||||||
          this.coupon_name,
 | 
					          this.coupon_name,
 | 
				
			||||||
 | 
					          this.coupon_type,
 | 
				
			||||||
          this.can,
 | 
					          this.can,
 | 
				
			||||||
          this.validity,
 | 
					          this.validity,
 | 
				
			||||||
          this.is_multiple_use,
 | 
					          this.is_multiple_use,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -21,7 +21,7 @@
 | 
				
			|||||||
              <v-col>
 | 
					              <v-col>
 | 
				
			||||||
                <v-btn
 | 
					                <v-btn
 | 
				
			||||||
                  block
 | 
					                  block
 | 
				
			||||||
                  color="#9970ff"
 | 
					                  color="#3bb9f1"
 | 
				
			||||||
                  dark
 | 
					                  dark
 | 
				
			||||||
                  depressed
 | 
					                  depressed
 | 
				
			||||||
                  v-bind="attrs"
 | 
					                  v-bind="attrs"
 | 
				
			||||||
@@ -39,16 +39,16 @@
 | 
				
			|||||||
                  class="elevation-1"
 | 
					                  class="elevation-1"
 | 
				
			||||||
                  hide-default-footer
 | 
					                  hide-default-footer
 | 
				
			||||||
                >
 | 
					                >
 | 
				
			||||||
                  <template v-slot:item.price="{ item }">
 | 
					                  <template v-slot:item.priceStr="{ item }">
 | 
				
			||||||
                    {{ item.price.toLocaleString('en-US') }} 원
 | 
					                    {{ formatMoney(item.priceStr, item.currency) }}
 | 
				
			||||||
                  </template>
 | 
					                  </template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                  <template v-slot:item.can="{ item }">
 | 
					                  <template v-slot:item.can="{ item }">
 | 
				
			||||||
                    {{ item.can.toLocaleString('en-US') }} 캔
 | 
					                    {{ formatNumber(item.can) }} 캔
 | 
				
			||||||
                  </template>
 | 
					                  </template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                  <template v-slot:item.rewardCan="{ item }">
 | 
					                  <template v-slot:item.rewardCan="{ item }">
 | 
				
			||||||
                    {{ item.rewardCan.toLocaleString('en-US') }} 캔
 | 
					                    {{ formatNumber(item.rewardCan) }} 캔
 | 
				
			||||||
                  </template>
 | 
					                  </template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                  <template v-slot:item.management="{ item }">
 | 
					                  <template v-slot:item.management="{ item }">
 | 
				
			||||||
@@ -70,7 +70,13 @@
 | 
				
			|||||||
          <v-card-text>
 | 
					          <v-card-text>
 | 
				
			||||||
            <v-text-field
 | 
					            <v-text-field
 | 
				
			||||||
              v-model="price"
 | 
					              v-model="price"
 | 
				
			||||||
              label="원화"
 | 
					              label="가격"
 | 
				
			||||||
 | 
					              required
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					            <v-select
 | 
				
			||||||
 | 
					              v-model="currency"
 | 
				
			||||||
 | 
					              :items="currencies"
 | 
				
			||||||
 | 
					              label="화폐 단위"
 | 
				
			||||||
              required
 | 
					              required
 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
          </v-card-text>
 | 
					          </v-card-text>
 | 
				
			||||||
@@ -125,12 +131,17 @@ export default {
 | 
				
			|||||||
      price: null,
 | 
					      price: null,
 | 
				
			||||||
      can: null,
 | 
					      can: null,
 | 
				
			||||||
      reward_can: null,
 | 
					      reward_can: null,
 | 
				
			||||||
 | 
					      currency: 'KRW',
 | 
				
			||||||
 | 
					      currencies: [
 | 
				
			||||||
 | 
					        { text: 'KRW (한국 원)', value: 'KRW' },
 | 
				
			||||||
 | 
					        { text: 'USD (미국 달러)', value: 'USD' }
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
      headers: [
 | 
					      headers: [
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          text: '원화(VAT포함)',
 | 
					          text: '가격(VAT포함)',
 | 
				
			||||||
          align: 'center',
 | 
					          align: 'center',
 | 
				
			||||||
          sortable: false,
 | 
					          sortable: false,
 | 
				
			||||||
          value: 'price',
 | 
					          value: 'priceStr',
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          text: '충전캔',
 | 
					          text: '충전캔',
 | 
				
			||||||
@@ -173,9 +184,26 @@ export default {
 | 
				
			|||||||
      this.can = null
 | 
					      this.can = null
 | 
				
			||||||
      this.price = null
 | 
					      this.price = null
 | 
				
			||||||
      this.reward_can = null
 | 
					      this.reward_can = null
 | 
				
			||||||
 | 
					      this.currency = 'KRW'
 | 
				
			||||||
      this.selected_can = null
 | 
					      this.selected_can = null
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    formatMoney(priceStr, currencyCode, locale = navigator.language) {
 | 
				
			||||||
 | 
					      const price = Number(priceStr);
 | 
				
			||||||
 | 
					      const formatted = new Intl.NumberFormat(locale, {
 | 
				
			||||||
 | 
					        style: 'currency',
 | 
				
			||||||
 | 
					        currency: currencyCode
 | 
				
			||||||
 | 
					      }).format(price);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return formatted.replace(/([^\d\s])(\d)/, '$1 $2');
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    formatNumber(num) {
 | 
				
			||||||
 | 
					      return new Intl.NumberFormat(navigator.language, {
 | 
				
			||||||
 | 
					        style: 'decimal'
 | 
				
			||||||
 | 
					      }).format(num);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async getCans() {
 | 
					    async getCans() {
 | 
				
			||||||
      this.isLoading = true
 | 
					      this.isLoading = true
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
@@ -204,13 +232,14 @@ export default {
 | 
				
			|||||||
    async submit() {
 | 
					    async submit() {
 | 
				
			||||||
      this.isLoading = true
 | 
					      this.isLoading = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const res = await api.insertCan(this.can, this.reward_can, this.price)
 | 
					      const res = await api.insertCan(this.can, this.reward_can, this.price, this.currency)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (res.status === 200 && res.data.success === true) {
 | 
					      if (res.status === 200 && res.data.success === true) {
 | 
				
			||||||
        this.show_dialog = false
 | 
					        this.show_dialog = false
 | 
				
			||||||
        this.can = null
 | 
					        this.can = null
 | 
				
			||||||
        this.price = null
 | 
					        this.price = null
 | 
				
			||||||
        this.reward_can = null
 | 
					        this.reward_can = null
 | 
				
			||||||
 | 
					        this.currency = 'KRW'
 | 
				
			||||||
        this.selected_can = null
 | 
					        this.selected_can = null
 | 
				
			||||||
        this.notifySuccess(res.data.message || '등록되었습니다.')
 | 
					        this.notifySuccess(res.data.message || '등록되었습니다.')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -62,7 +62,7 @@
 | 
				
			|||||||
            </template>
 | 
					            </template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <template v-slot:item.chargeAmount="{ item }">
 | 
					            <template v-slot:item.chargeAmount="{ item }">
 | 
				
			||||||
              {{ item.chargeAmount.toLocaleString() }} 원
 | 
					              {{ formatMoney(item.chargeAmount, item.currency) }}
 | 
				
			||||||
            </template>
 | 
					            </template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <template v-slot:item.locale="{ item }">
 | 
					            <template v-slot:item.locale="{ item }">
 | 
				
			||||||
@@ -107,7 +107,7 @@
 | 
				
			|||||||
              </template>
 | 
					              </template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              <template v-slot:item.amount="{ item }">
 | 
					              <template v-slot:item.amount="{ item }">
 | 
				
			||||||
                {{ item.amount.toLocaleString() }} 원
 | 
					                {{ formatMoney(item.amount, item.locale) }}
 | 
				
			||||||
              </template>
 | 
					              </template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              <template v-slot:item.datetime="{ item }">
 | 
					              <template v-slot:item.datetime="{ item }">
 | 
				
			||||||
@@ -204,6 +204,12 @@ export default {
 | 
				
			|||||||
          sortable: false,
 | 
					          sortable: false,
 | 
				
			||||||
          value: 'chargeCount',
 | 
					          value: 'chargeCount',
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          text: '화폐단위',
 | 
				
			||||||
 | 
					          align: 'center',
 | 
				
			||||||
 | 
					          sortable: false,
 | 
				
			||||||
 | 
					          value: 'currency',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          text: 'PG',
 | 
					          text: 'PG',
 | 
				
			||||||
          sortable: false,
 | 
					          sortable: false,
 | 
				
			||||||
@@ -248,6 +254,15 @@ export default {
 | 
				
			|||||||
      this.show_popup_dialog = false
 | 
					      this.show_popup_dialog = false
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    formatMoney(price, currencyCode, locale = navigator.language) {
 | 
				
			||||||
 | 
					      const formatted = new Intl.NumberFormat(locale, {
 | 
				
			||||||
 | 
					        style: 'currency',
 | 
				
			||||||
 | 
					        currency: currencyCode
 | 
				
			||||||
 | 
					      }).format(price);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return formatted.replace(/([^\d\s])(\d)/, '$1 $2');
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async getChargeStatus() {
 | 
					    async getChargeStatus() {
 | 
				
			||||||
      this.is_loading = true
 | 
					      this.is_loading = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -271,7 +286,7 @@ export default {
 | 
				
			|||||||
        this.is_loading = true
 | 
					        this.is_loading = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
          const res = await api.getChargeStatusDetail(value.date, value.pg)
 | 
					          const res = await api.getChargeStatusDetail(value.date, value.pg, value.currency)
 | 
				
			||||||
          if (res.status === 200 && res.data.success === true) {
 | 
					          if (res.status === 200 && res.data.success === true) {
 | 
				
			||||||
            this.detail_items = res.data.data
 | 
					            this.detail_items = res.data.data
 | 
				
			||||||
            this.show_popup_dialog = true
 | 
					            this.show_popup_dialog = true
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										583
									
								
								src/views/Chat/CharacterBanner.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										583
									
								
								src/views/Chat/CharacterBanner.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,583 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div>
 | 
				
			||||||
 | 
					    <v-toolbar dark>
 | 
				
			||||||
 | 
					      <v-btn
 | 
				
			||||||
 | 
					        icon
 | 
				
			||||||
 | 
					        @click="goBack"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <v-icon>mdi-arrow-left</v-icon>
 | 
				
			||||||
 | 
					      </v-btn>
 | 
				
			||||||
 | 
					      <v-spacer />
 | 
				
			||||||
 | 
					      <v-toolbar-title>캐릭터 배너 관리</v-toolbar-title>
 | 
				
			||||||
 | 
					      <v-spacer />
 | 
				
			||||||
 | 
					    </v-toolbar>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <v-container>
 | 
				
			||||||
 | 
					      <v-row>
 | 
				
			||||||
 | 
					        <v-col cols="4">
 | 
				
			||||||
 | 
					          <v-btn
 | 
				
			||||||
 | 
					            color="primary"
 | 
				
			||||||
 | 
					            dark
 | 
				
			||||||
 | 
					            @click="showAddDialog"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            배너 추가
 | 
				
			||||||
 | 
					          </v-btn>
 | 
				
			||||||
 | 
					        </v-col>
 | 
				
			||||||
 | 
					        <v-spacer />
 | 
				
			||||||
 | 
					      </v-row>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <!-- 로딩 표시 -->
 | 
				
			||||||
 | 
					      <v-row v-if="isLoading && banners.length === 0">
 | 
				
			||||||
 | 
					        <v-col class="text-center">
 | 
				
			||||||
 | 
					          <v-progress-circular
 | 
				
			||||||
 | 
					            indeterminate
 | 
				
			||||||
 | 
					            color="primary"
 | 
				
			||||||
 | 
					            size="64"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </v-col>
 | 
				
			||||||
 | 
					      </v-row>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <!-- 배너 그리드 -->
 | 
				
			||||||
 | 
					      <v-row>
 | 
				
			||||||
 | 
					        <draggable
 | 
				
			||||||
 | 
					          v-model="banners"
 | 
				
			||||||
 | 
					          class="row"
 | 
				
			||||||
 | 
					          style="width: 100%"
 | 
				
			||||||
 | 
					          :options="{ animation: 150 }"
 | 
				
			||||||
 | 
					          @end="onDragEnd"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <v-col
 | 
				
			||||||
 | 
					            v-for="banner in banners"
 | 
				
			||||||
 | 
					            :key="banner.id"
 | 
				
			||||||
 | 
					            cols="12"
 | 
				
			||||||
 | 
					            sm="6"
 | 
				
			||||||
 | 
					            md="4"
 | 
				
			||||||
 | 
					            lg="3"
 | 
				
			||||||
 | 
					            class="banner-item"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <v-card
 | 
				
			||||||
 | 
					              class="mx-auto"
 | 
				
			||||||
 | 
					              max-width="300"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <v-img
 | 
				
			||||||
 | 
					                :src="banner.imagePath"
 | 
				
			||||||
 | 
					                height="200"
 | 
				
			||||||
 | 
					                contain
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					              <v-card-text class="text-center">
 | 
				
			||||||
 | 
					                <div>{{ banner.characterName }}</div>
 | 
				
			||||||
 | 
					              </v-card-text>
 | 
				
			||||||
 | 
					              <v-card-actions>
 | 
				
			||||||
 | 
					                <v-spacer />
 | 
				
			||||||
 | 
					                <v-btn
 | 
				
			||||||
 | 
					                  small
 | 
				
			||||||
 | 
					                  color="primary"
 | 
				
			||||||
 | 
					                  @click="showEditDialog(banner)"
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  수정
 | 
				
			||||||
 | 
					                </v-btn>
 | 
				
			||||||
 | 
					                <v-btn
 | 
				
			||||||
 | 
					                  small
 | 
				
			||||||
 | 
					                  color="error"
 | 
				
			||||||
 | 
					                  @click="confirmDelete(banner)"
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  삭제
 | 
				
			||||||
 | 
					                </v-btn>
 | 
				
			||||||
 | 
					                <v-spacer />
 | 
				
			||||||
 | 
					              </v-card-actions>
 | 
				
			||||||
 | 
					            </v-card>
 | 
				
			||||||
 | 
					          </v-col>
 | 
				
			||||||
 | 
					        </draggable>
 | 
				
			||||||
 | 
					      </v-row>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <!-- 데이터가 없을 때 표시 -->
 | 
				
			||||||
 | 
					      <v-row v-if="!isLoading && banners.length === 0">
 | 
				
			||||||
 | 
					        <v-col class="text-center">
 | 
				
			||||||
 | 
					          <p>등록된 배너가 없습니다.</p>
 | 
				
			||||||
 | 
					        </v-col>
 | 
				
			||||||
 | 
					      </v-row>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <!-- 무한 스크롤 로딩 -->
 | 
				
			||||||
 | 
					      <v-row v-if="isLoading && banners.length > 0">
 | 
				
			||||||
 | 
					        <v-col class="text-center">
 | 
				
			||||||
 | 
					          <v-progress-circular
 | 
				
			||||||
 | 
					            indeterminate
 | 
				
			||||||
 | 
					            color="primary"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </v-col>
 | 
				
			||||||
 | 
					      </v-row>
 | 
				
			||||||
 | 
					    </v-container>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <!-- 배너 추가/수정 다이얼로그 -->
 | 
				
			||||||
 | 
					    <v-dialog
 | 
				
			||||||
 | 
					      v-model="showDialog"
 | 
				
			||||||
 | 
					      max-width="600px"
 | 
				
			||||||
 | 
					      persistent
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <v-card>
 | 
				
			||||||
 | 
					        <v-card-title>
 | 
				
			||||||
 | 
					          <span class="headline">{{ isEdit ? '배너 수정' : '배너 추가' }}</span>
 | 
				
			||||||
 | 
					        </v-card-title>
 | 
				
			||||||
 | 
					        <v-card-text>
 | 
				
			||||||
 | 
					          <v-container>
 | 
				
			||||||
 | 
					            <v-row>
 | 
				
			||||||
 | 
					              <v-col cols="12">
 | 
				
			||||||
 | 
					                <v-file-input
 | 
				
			||||||
 | 
					                  v-model="bannerForm.image"
 | 
				
			||||||
 | 
					                  label="배너 이미지"
 | 
				
			||||||
 | 
					                  accept="image/*"
 | 
				
			||||||
 | 
					                  prepend-icon="mdi-camera"
 | 
				
			||||||
 | 
					                  show-size
 | 
				
			||||||
 | 
					                  truncate-length="15"
 | 
				
			||||||
 | 
					                  :rules="imageRules"
 | 
				
			||||||
 | 
					                  outlined
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              </v-col>
 | 
				
			||||||
 | 
					            </v-row>
 | 
				
			||||||
 | 
					            <v-row v-if="previewImage || (isEdit && bannerForm.imageUrl)">
 | 
				
			||||||
 | 
					              <v-col
 | 
				
			||||||
 | 
					                cols="12"
 | 
				
			||||||
 | 
					                class="text-center"
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <v-img
 | 
				
			||||||
 | 
					                  :src="previewImage || bannerForm.imageUrl"
 | 
				
			||||||
 | 
					                  max-height="200"
 | 
				
			||||||
 | 
					                  contain
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              </v-col>
 | 
				
			||||||
 | 
					            </v-row>
 | 
				
			||||||
 | 
					            <v-row>
 | 
				
			||||||
 | 
					              <v-col cols="12">
 | 
				
			||||||
 | 
					                <v-text-field
 | 
				
			||||||
 | 
					                  v-model="searchKeyword"
 | 
				
			||||||
 | 
					                  label="캐릭터 검색"
 | 
				
			||||||
 | 
					                  outlined
 | 
				
			||||||
 | 
					                  @keyup.enter="searchCharacter"
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              </v-col>
 | 
				
			||||||
 | 
					            </v-row>
 | 
				
			||||||
 | 
					            <v-row v-if="searchResults.length > 0">
 | 
				
			||||||
 | 
					              <v-col cols="12">
 | 
				
			||||||
 | 
					                <v-list>
 | 
				
			||||||
 | 
					                  <v-list-item
 | 
				
			||||||
 | 
					                    v-for="character in searchResults"
 | 
				
			||||||
 | 
					                    :key="character.id"
 | 
				
			||||||
 | 
					                    @click="selectCharacter(character)"
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                    <v-list-item-avatar>
 | 
				
			||||||
 | 
					                      <v-img :src="character.imageUrl" />
 | 
				
			||||||
 | 
					                    </v-list-item-avatar>
 | 
				
			||||||
 | 
					                    <v-list-item-content>
 | 
				
			||||||
 | 
					                      <v-list-item-title>{{ character.name }}</v-list-item-title>
 | 
				
			||||||
 | 
					                    </v-list-item-content>
 | 
				
			||||||
 | 
					                  </v-list-item>
 | 
				
			||||||
 | 
					                </v-list>
 | 
				
			||||||
 | 
					              </v-col>
 | 
				
			||||||
 | 
					            </v-row>
 | 
				
			||||||
 | 
					            <v-row v-if="searchPerformed && searchResults.length === 0">
 | 
				
			||||||
 | 
					              <v-col cols="12">
 | 
				
			||||||
 | 
					                <v-alert
 | 
				
			||||||
 | 
					                  type="info"
 | 
				
			||||||
 | 
					                  outlined
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  검색결과가 없습니다.
 | 
				
			||||||
 | 
					                </v-alert>
 | 
				
			||||||
 | 
					              </v-col>
 | 
				
			||||||
 | 
					            </v-row>
 | 
				
			||||||
 | 
					            <v-row v-if="selectedCharacter">
 | 
				
			||||||
 | 
					              <v-col cols="12">
 | 
				
			||||||
 | 
					                <v-alert
 | 
				
			||||||
 | 
					                  type="info"
 | 
				
			||||||
 | 
					                  outlined
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  <v-row align="center">
 | 
				
			||||||
 | 
					                    <v-col cols="auto">
 | 
				
			||||||
 | 
					                      <v-avatar size="50">
 | 
				
			||||||
 | 
					                        <v-img :src="selectedCharacter.imageUrl" />
 | 
				
			||||||
 | 
					                      </v-avatar>
 | 
				
			||||||
 | 
					                    </v-col>
 | 
				
			||||||
 | 
					                    <v-col>
 | 
				
			||||||
 | 
					                      <div class="font-weight-medium">
 | 
				
			||||||
 | 
					                        선택된 캐릭터: {{ selectedCharacter.name }}
 | 
				
			||||||
 | 
					                      </div>
 | 
				
			||||||
 | 
					                    </v-col>
 | 
				
			||||||
 | 
					                  </v-row>
 | 
				
			||||||
 | 
					                </v-alert>
 | 
				
			||||||
 | 
					              </v-col>
 | 
				
			||||||
 | 
					            </v-row>
 | 
				
			||||||
 | 
					          </v-container>
 | 
				
			||||||
 | 
					        </v-card-text>
 | 
				
			||||||
 | 
					        <v-card-actions>
 | 
				
			||||||
 | 
					          <v-spacer />
 | 
				
			||||||
 | 
					          <v-btn
 | 
				
			||||||
 | 
					            color="blue darken-1"
 | 
				
			||||||
 | 
					            text
 | 
				
			||||||
 | 
					            @click="closeDialog"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            취소
 | 
				
			||||||
 | 
					          </v-btn>
 | 
				
			||||||
 | 
					          <v-btn
 | 
				
			||||||
 | 
					            color="blue darken-1"
 | 
				
			||||||
 | 
					            text
 | 
				
			||||||
 | 
					            :disabled="!isFormValid || isSubmitting"
 | 
				
			||||||
 | 
					            :loading="isSubmitting"
 | 
				
			||||||
 | 
					            @click="saveBanner"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            저장
 | 
				
			||||||
 | 
					          </v-btn>
 | 
				
			||||||
 | 
					        </v-card-actions>
 | 
				
			||||||
 | 
					      </v-card>
 | 
				
			||||||
 | 
					    </v-dialog>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <!-- 삭제 확인 다이얼로그 -->
 | 
				
			||||||
 | 
					    <v-dialog
 | 
				
			||||||
 | 
					      v-model="showDeleteDialog"
 | 
				
			||||||
 | 
					      max-width="400"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <v-card>
 | 
				
			||||||
 | 
					        <v-card-title class="headline">
 | 
				
			||||||
 | 
					          배너 삭제
 | 
				
			||||||
 | 
					        </v-card-title>
 | 
				
			||||||
 | 
					        <v-card-text>
 | 
				
			||||||
 | 
					          삭제 할까요?
 | 
				
			||||||
 | 
					        </v-card-text>
 | 
				
			||||||
 | 
					        <v-card-actions>
 | 
				
			||||||
 | 
					          <v-spacer />
 | 
				
			||||||
 | 
					          <v-btn
 | 
				
			||||||
 | 
					            color="blue darken-1"
 | 
				
			||||||
 | 
					            text
 | 
				
			||||||
 | 
					            @click="showDeleteDialog = false"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            취소
 | 
				
			||||||
 | 
					          </v-btn>
 | 
				
			||||||
 | 
					          <v-btn
 | 
				
			||||||
 | 
					            color="red darken-1"
 | 
				
			||||||
 | 
					            text
 | 
				
			||||||
 | 
					            :loading="isSubmitting"
 | 
				
			||||||
 | 
					            @click="deleteBanner"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            삭제
 | 
				
			||||||
 | 
					          </v-btn>
 | 
				
			||||||
 | 
					        </v-card-actions>
 | 
				
			||||||
 | 
					      </v-card>
 | 
				
			||||||
 | 
					    </v-dialog>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  getCharacterBannerList,
 | 
				
			||||||
 | 
					  createCharacterBanner,
 | 
				
			||||||
 | 
					  updateCharacterBanner,
 | 
				
			||||||
 | 
					  deleteCharacterBanner,
 | 
				
			||||||
 | 
					  updateCharacterBannerOrder,
 | 
				
			||||||
 | 
					  searchCharacters
 | 
				
			||||||
 | 
					} from '@/api/character';
 | 
				
			||||||
 | 
					import draggable from 'vuedraggable';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default {
 | 
				
			||||||
 | 
					  name: 'CharacterBanner',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  components: {
 | 
				
			||||||
 | 
					    draggable
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  data() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      isLoading: false,
 | 
				
			||||||
 | 
					      isSubmitting: false,
 | 
				
			||||||
 | 
					      banners: [],
 | 
				
			||||||
 | 
					      page: 1,
 | 
				
			||||||
 | 
					      hasMoreItems: true,
 | 
				
			||||||
 | 
					      showDialog: false,
 | 
				
			||||||
 | 
					      showDeleteDialog: false,
 | 
				
			||||||
 | 
					      isEdit: false,
 | 
				
			||||||
 | 
					      selectedBanner: null,
 | 
				
			||||||
 | 
					      selectedCharacter: null,
 | 
				
			||||||
 | 
					      searchKeyword: '',
 | 
				
			||||||
 | 
					      searchResults: [],
 | 
				
			||||||
 | 
					      searchPerformed: false,
 | 
				
			||||||
 | 
					      previewImage: null,
 | 
				
			||||||
 | 
					      bannerForm: {
 | 
				
			||||||
 | 
					        image: null,
 | 
				
			||||||
 | 
					        imageUrl: '',
 | 
				
			||||||
 | 
					        characterId: null,
 | 
				
			||||||
 | 
					        bannerId: null
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      imageRules: [
 | 
				
			||||||
 | 
					        v => !!v || this.isEdit || '이미지를 선택하세요'
 | 
				
			||||||
 | 
					      ]
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  computed: {
 | 
				
			||||||
 | 
					    isFormValid() {
 | 
				
			||||||
 | 
					      return (this.bannerForm.image || (this.isEdit && this.bannerForm.imageUrl)) && this.selectedCharacter;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  watch: {
 | 
				
			||||||
 | 
					    'bannerForm.image': {
 | 
				
			||||||
 | 
					      handler(newImage) {
 | 
				
			||||||
 | 
					        if (newImage) {
 | 
				
			||||||
 | 
					          this.createImagePreview(newImage);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          this.previewImage = null;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  mounted() {
 | 
				
			||||||
 | 
					    this.loadBanners();
 | 
				
			||||||
 | 
					    window.addEventListener('scroll', this.handleScroll);
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeDestroy() {
 | 
				
			||||||
 | 
					    window.removeEventListener('scroll', this.handleScroll);
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  methods: {
 | 
				
			||||||
 | 
					    notifyError(message) {
 | 
				
			||||||
 | 
					      this.$dialog.notify.error(message);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    notifySuccess(message) {
 | 
				
			||||||
 | 
					      this.$dialog.notify.success(message);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    goBack() {
 | 
				
			||||||
 | 
					      this.$router.push('/character');
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async loadBanners() {
 | 
				
			||||||
 | 
					      if (this.isLoading || !this.hasMoreItems) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      this.isLoading = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const response = await getCharacterBannerList(this.page);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (response && response.status === 200 && response.data && response.data.success === true) {
 | 
				
			||||||
 | 
					          const data = response.data.data;
 | 
				
			||||||
 | 
					          const newBanners = data.content || [];
 | 
				
			||||||
 | 
					          this.banners = [...this.banners, ...newBanners];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          // 더 불러올 데이터가 있는지 확인
 | 
				
			||||||
 | 
					          this.hasMoreItems = newBanners.length > 0;
 | 
				
			||||||
 | 
					          this.page++;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          this.notifyError('배너 목록을 불러오는데 실패했습니다.');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch (error) {
 | 
				
			||||||
 | 
					        this.notifyError('배너 목록을 불러오는데 실패했습니다.');
 | 
				
			||||||
 | 
					      } finally {
 | 
				
			||||||
 | 
					        this.isLoading = false;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    handleScroll() {
 | 
				
			||||||
 | 
					      const scrollPosition = window.innerHeight + window.scrollY;
 | 
				
			||||||
 | 
					      const documentHeight = document.documentElement.offsetHeight;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // 스크롤이 페이지 하단에 도달하면 추가 데이터 로드
 | 
				
			||||||
 | 
					      if (scrollPosition >= documentHeight - 200 && !this.isLoading && this.hasMoreItems) {
 | 
				
			||||||
 | 
					        this.loadBanners();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    showAddDialog() {
 | 
				
			||||||
 | 
					      this.isEdit = false;
 | 
				
			||||||
 | 
					      this.selectedCharacter = null;
 | 
				
			||||||
 | 
					      this.bannerForm = {
 | 
				
			||||||
 | 
					        image: null,
 | 
				
			||||||
 | 
					        imageUrl: '',
 | 
				
			||||||
 | 
					        characterId: null,
 | 
				
			||||||
 | 
					        bannerId: null
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					      this.previewImage = null;
 | 
				
			||||||
 | 
					      this.searchKeyword = '';
 | 
				
			||||||
 | 
					      this.searchResults = [];
 | 
				
			||||||
 | 
					      this.searchPerformed = false;
 | 
				
			||||||
 | 
					      this.showDialog = true;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    showEditDialog(banner) {
 | 
				
			||||||
 | 
					      this.isEdit = true;
 | 
				
			||||||
 | 
					      this.selectedBanner = banner;
 | 
				
			||||||
 | 
					      this.selectedCharacter = {
 | 
				
			||||||
 | 
					        id: banner.characterId,
 | 
				
			||||||
 | 
					        name: banner.characterName,
 | 
				
			||||||
 | 
					        imageUrl: banner.characterImageUrl
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					      this.bannerForm = {
 | 
				
			||||||
 | 
					        image: null,
 | 
				
			||||||
 | 
					        imageUrl: banner.imageUrl,
 | 
				
			||||||
 | 
					        characterId: banner.characterId,
 | 
				
			||||||
 | 
					        bannerId: banner.id
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					      this.previewImage = null;
 | 
				
			||||||
 | 
					      this.searchKeyword = '';
 | 
				
			||||||
 | 
					      this.searchResults = [];
 | 
				
			||||||
 | 
					      this.searchPerformed = false;
 | 
				
			||||||
 | 
					      this.showDialog = true;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    closeDialog() {
 | 
				
			||||||
 | 
					      this.showDialog = false;
 | 
				
			||||||
 | 
					      this.selectedCharacter = null;
 | 
				
			||||||
 | 
					      this.bannerForm = {
 | 
				
			||||||
 | 
					        image: null,
 | 
				
			||||||
 | 
					        imageUrl: '',
 | 
				
			||||||
 | 
					        characterId: null,
 | 
				
			||||||
 | 
					        bannerId: null
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					      this.previewImage = null;
 | 
				
			||||||
 | 
					      this.searchKeyword = '';
 | 
				
			||||||
 | 
					      this.searchResults = [];
 | 
				
			||||||
 | 
					      this.searchPerformed = false;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    confirmDelete(banner) {
 | 
				
			||||||
 | 
					      this.selectedBanner = banner;
 | 
				
			||||||
 | 
					      this.showDeleteDialog = true;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    createImagePreview(file) {
 | 
				
			||||||
 | 
					      if (!file) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const reader = new FileReader();
 | 
				
			||||||
 | 
					      reader.onload = (e) => {
 | 
				
			||||||
 | 
					        this.previewImage = e.target.result;
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					      reader.readAsDataURL(file);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async searchCharacter() {
 | 
				
			||||||
 | 
					      if (!this.searchKeyword || this.searchKeyword.length < 2) {
 | 
				
			||||||
 | 
					        this.notifyError('검색어를 2글자 이상 입력하세요.');
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const response = await searchCharacters(this.searchKeyword);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (response && response.status === 200 && response.data && response.data.success === true) {
 | 
				
			||||||
 | 
					          const data = response.data.data;
 | 
				
			||||||
 | 
					          this.searchResults = data.content || [];
 | 
				
			||||||
 | 
					          this.searchPerformed = true;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch (error) {
 | 
				
			||||||
 | 
					        console.error('캐릭터 검색 오류:', error);
 | 
				
			||||||
 | 
					        this.notifyError('캐릭터 검색에 실패했습니다.');
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    selectCharacter(character) {
 | 
				
			||||||
 | 
					      this.selectedCharacter = character;
 | 
				
			||||||
 | 
					      this.bannerForm.characterId = character.id;
 | 
				
			||||||
 | 
					      this.searchResults = [];
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async saveBanner() {
 | 
				
			||||||
 | 
					      if (!this.isFormValid || this.isSubmitting) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      this.isSubmitting = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        if (this.isEdit) {
 | 
				
			||||||
 | 
					          // 배너 수정
 | 
				
			||||||
 | 
					          const response = await updateCharacterBanner({
 | 
				
			||||||
 | 
					            image: this.bannerForm.image,
 | 
				
			||||||
 | 
					            characterId: this.selectedCharacter.id,
 | 
				
			||||||
 | 
					            bannerId: this.bannerForm.bannerId
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					          if (response && response.status === 200 && response.data && response.data.success === true) {
 | 
				
			||||||
 | 
					            this.notifySuccess('배너가 수정되었습니다.');
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            this.notifyError('배너 수정을 실패했습니다.');
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          // 배너 추가
 | 
				
			||||||
 | 
					          const response = await createCharacterBanner({
 | 
				
			||||||
 | 
					            image: this.bannerForm.image,
 | 
				
			||||||
 | 
					            characterId: this.selectedCharacter.id
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					          if (response && response.status === 200 && response.data && response.data.success === true) {
 | 
				
			||||||
 | 
					            this.notifySuccess('배너가 추가되었습니다.');
 | 
				
			||||||
 | 
					            // 다이얼로그 닫고 배너 목록 새로고침
 | 
				
			||||||
 | 
					            this.closeDialog();
 | 
				
			||||||
 | 
					            this.refreshBanners();
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            this.notifyError('배너 추가를 실패했습니다.');
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch (error) {
 | 
				
			||||||
 | 
					        console.error('배너 저장 오류:', error);
 | 
				
			||||||
 | 
					        this.notifyError('배너 저장에 실패했습니다.');
 | 
				
			||||||
 | 
					      } finally {
 | 
				
			||||||
 | 
					        this.isSubmitting = false;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async deleteBanner() {
 | 
				
			||||||
 | 
					      if (!this.selectedBanner || this.isSubmitting) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      this.isSubmitting = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const response = await deleteCharacterBanner(this.selectedBanner.id);
 | 
				
			||||||
 | 
					        if (response && response.status === 200 && response.data && response.data.success === true) {
 | 
				
			||||||
 | 
					          this.notifySuccess('배너가 삭제되었습니다.');
 | 
				
			||||||
 | 
					          this.showDeleteDialog = false;
 | 
				
			||||||
 | 
					          this.refreshBanners();
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          this.notifyError('배너 삭제에 실패했습니다.');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch (error) {
 | 
				
			||||||
 | 
					        console.error('배너 삭제 오류:', error);
 | 
				
			||||||
 | 
					        this.notifyError('배너 삭제에 실패했습니다.');
 | 
				
			||||||
 | 
					      } finally {
 | 
				
			||||||
 | 
					        this.isSubmitting = false;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    refreshBanners() {
 | 
				
			||||||
 | 
					      // 배너 목록 초기화 후 다시 로드
 | 
				
			||||||
 | 
					      this.banners = [];
 | 
				
			||||||
 | 
					      this.page = 1;
 | 
				
			||||||
 | 
					      this.hasMoreItems = true;
 | 
				
			||||||
 | 
					      this.loadBanners();
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async onDragEnd() {
 | 
				
			||||||
 | 
					      // 드래그 앤 드롭으로 순서 변경 후 API 호출
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const bannerIds = this.banners.map(banner => banner.id);
 | 
				
			||||||
 | 
					        const response = await updateCharacterBannerOrder(bannerIds);
 | 
				
			||||||
 | 
					        if (response && response.status === 200 && response.data && response.data.success === true) {
 | 
				
			||||||
 | 
					          this.notifySuccess('배너 순서가 변경되었습니다.');
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          this.notifyError('배너 순서 변경에 실패했습니다.');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch (error) {
 | 
				
			||||||
 | 
					        console.error('배너 순서 변경 오류:', error);
 | 
				
			||||||
 | 
					        this.notifyError('배너 순서 변경에 실패했습니다.');
 | 
				
			||||||
 | 
					        // 실패 시 목록 새로고침
 | 
				
			||||||
 | 
					        this.refreshBanners();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style scoped>
 | 
				
			||||||
 | 
					.banner-item {
 | 
				
			||||||
 | 
					  transition: all 0.3s;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.banner-item:hover {
 | 
				
			||||||
 | 
					  transform: translateY(-5px);
 | 
				
			||||||
 | 
					  box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										315
									
								
								src/views/Chat/CharacterCalculateList.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										315
									
								
								src/views/Chat/CharacterCalculateList.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,315 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div>
 | 
				
			||||||
 | 
					    <v-toolbar dark>
 | 
				
			||||||
 | 
					      <v-spacer />
 | 
				
			||||||
 | 
					      <v-toolbar-title>캐릭터 정산</v-toolbar-title>
 | 
				
			||||||
 | 
					      <v-spacer />
 | 
				
			||||||
 | 
					    </v-toolbar>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <v-container>
 | 
				
			||||||
 | 
					      <v-row class="justify-center align-center text-center">
 | 
				
			||||||
 | 
					        <v-col
 | 
				
			||||||
 | 
					          cols="12"
 | 
				
			||||||
 | 
					          md="3"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <v-menu
 | 
				
			||||||
 | 
					            ref="menuStart"
 | 
				
			||||||
 | 
					            v-model="menuStart"
 | 
				
			||||||
 | 
					            :close-on-content-click="false"
 | 
				
			||||||
 | 
					            transition="scale-transition"
 | 
				
			||||||
 | 
					            offset-y
 | 
				
			||||||
 | 
					            min-width="auto"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <template v-slot:activator="{ on, attrs }">
 | 
				
			||||||
 | 
					              <v-text-field
 | 
				
			||||||
 | 
					                v-model="filters.startDateStr"
 | 
				
			||||||
 | 
					                label="시작일"
 | 
				
			||||||
 | 
					                readonly
 | 
				
			||||||
 | 
					                dense
 | 
				
			||||||
 | 
					                v-bind="attrs"
 | 
				
			||||||
 | 
					                clearable
 | 
				
			||||||
 | 
					                v-on="on"
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					            <v-date-picker
 | 
				
			||||||
 | 
					              v-model="filters.startDateStr"
 | 
				
			||||||
 | 
					              :max="filters.endDateStr && filters.endDateStr < todayStr ? filters.endDateStr : todayStr"
 | 
				
			||||||
 | 
					              locale="ko-kr"
 | 
				
			||||||
 | 
					              @input="$refs.menuStart.save(filters.startDateStr)"
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          </v-menu>
 | 
				
			||||||
 | 
					        </v-col>
 | 
				
			||||||
 | 
					        <v-col
 | 
				
			||||||
 | 
					          cols="12"
 | 
				
			||||||
 | 
					          md="3"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <v-menu
 | 
				
			||||||
 | 
					            ref="menuEnd"
 | 
				
			||||||
 | 
					            v-model="menuEnd"
 | 
				
			||||||
 | 
					            :close-on-content-click="false"
 | 
				
			||||||
 | 
					            transition="scale-transition"
 | 
				
			||||||
 | 
					            offset-y
 | 
				
			||||||
 | 
					            min-width="auto"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <template v-slot:activator="{ on, attrs }">
 | 
				
			||||||
 | 
					              <v-text-field
 | 
				
			||||||
 | 
					                v-model="filters.endDateStr"
 | 
				
			||||||
 | 
					                label="종료일"
 | 
				
			||||||
 | 
					                readonly
 | 
				
			||||||
 | 
					                dense
 | 
				
			||||||
 | 
					                v-bind="attrs"
 | 
				
			||||||
 | 
					                clearable
 | 
				
			||||||
 | 
					                v-on="on"
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					            <v-date-picker
 | 
				
			||||||
 | 
					              v-model="filters.endDateStr"
 | 
				
			||||||
 | 
					              :min="filters.startDateStr"
 | 
				
			||||||
 | 
					              :max="todayStr"
 | 
				
			||||||
 | 
					              locale="ko-kr"
 | 
				
			||||||
 | 
					              @input="$refs.menuEnd.save(filters.endDateStr)"
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          </v-menu>
 | 
				
			||||||
 | 
					        </v-col>
 | 
				
			||||||
 | 
					        <v-col
 | 
				
			||||||
 | 
					          cols="12"
 | 
				
			||||||
 | 
					          md="3"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <v-select
 | 
				
			||||||
 | 
					            v-model="filters.sort"
 | 
				
			||||||
 | 
					            :items="sortItems"
 | 
				
			||||||
 | 
					            label="정렬"
 | 
				
			||||||
 | 
					            item-text="text"
 | 
				
			||||||
 | 
					            item-value="value"
 | 
				
			||||||
 | 
					            dense
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </v-col>
 | 
				
			||||||
 | 
					        <v-col
 | 
				
			||||||
 | 
					          cols="12"
 | 
				
			||||||
 | 
					          md="3"
 | 
				
			||||||
 | 
					          class="d-flex justify-center align-center"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <v-btn
 | 
				
			||||||
 | 
					            color="primary"
 | 
				
			||||||
 | 
					            small
 | 
				
			||||||
 | 
					            :loading="is_loading"
 | 
				
			||||||
 | 
					            @click="fetchList(1)"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            조회
 | 
				
			||||||
 | 
					          </v-btn>
 | 
				
			||||||
 | 
					          <v-btn
 | 
				
			||||||
 | 
					            class="ml-2"
 | 
				
			||||||
 | 
					            text
 | 
				
			||||||
 | 
					            small
 | 
				
			||||||
 | 
					            @click="resetFilters"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            초기화
 | 
				
			||||||
 | 
					          </v-btn>
 | 
				
			||||||
 | 
					        </v-col>
 | 
				
			||||||
 | 
					      </v-row>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <v-row>
 | 
				
			||||||
 | 
					        <v-col>
 | 
				
			||||||
 | 
					          <v-simple-table class="elevation-10 text-center">
 | 
				
			||||||
 | 
					            <template>
 | 
				
			||||||
 | 
					              <thead>
 | 
				
			||||||
 | 
					                <tr>
 | 
				
			||||||
 | 
					                  <th class="text-center">
 | 
				
			||||||
 | 
					                    이미지
 | 
				
			||||||
 | 
					                  </th>
 | 
				
			||||||
 | 
					                  <th class="text-center">
 | 
				
			||||||
 | 
					                    캐릭터명
 | 
				
			||||||
 | 
					                  </th>
 | 
				
			||||||
 | 
					                  <th class="text-center">
 | 
				
			||||||
 | 
					                    이미지 단독 구매(캔)
 | 
				
			||||||
 | 
					                  </th>
 | 
				
			||||||
 | 
					                  <th class="text-center">
 | 
				
			||||||
 | 
					                    메시지 구매(캔)
 | 
				
			||||||
 | 
					                  </th>
 | 
				
			||||||
 | 
					                  <th class="text-center">
 | 
				
			||||||
 | 
					                    채팅 횟수 구매(캔)
 | 
				
			||||||
 | 
					                  </th>
 | 
				
			||||||
 | 
					                  <th class="text-center">
 | 
				
			||||||
 | 
					                    합계(캔)
 | 
				
			||||||
 | 
					                  </th>
 | 
				
			||||||
 | 
					                  <th class="text-center">
 | 
				
			||||||
 | 
					                    합계(원화)
 | 
				
			||||||
 | 
					                  </th>
 | 
				
			||||||
 | 
					                  <th class="text-center">
 | 
				
			||||||
 | 
					                    정산금액(원)
 | 
				
			||||||
 | 
					                  </th>
 | 
				
			||||||
 | 
					                </tr>
 | 
				
			||||||
 | 
					              </thead>
 | 
				
			||||||
 | 
					              <tbody>
 | 
				
			||||||
 | 
					                <tr
 | 
				
			||||||
 | 
					                  v-for="item in items"
 | 
				
			||||||
 | 
					                  :key="item.characterId"
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  <td align="center">
 | 
				
			||||||
 | 
					                    <v-img
 | 
				
			||||||
 | 
					                      :src="item.characterImage"
 | 
				
			||||||
 | 
					                      max-width="64"
 | 
				
			||||||
 | 
					                      max-height="64"
 | 
				
			||||||
 | 
					                      class="rounded-circle"
 | 
				
			||||||
 | 
					                      contain
 | 
				
			||||||
 | 
					                    />
 | 
				
			||||||
 | 
					                  </td>
 | 
				
			||||||
 | 
					                  <td class="text-center">
 | 
				
			||||||
 | 
					                    {{ item.name }}
 | 
				
			||||||
 | 
					                  </td>
 | 
				
			||||||
 | 
					                  <td class="text-center">
 | 
				
			||||||
 | 
					                    {{ formatNumber(item.imagePurchaseCan) }}
 | 
				
			||||||
 | 
					                  </td>
 | 
				
			||||||
 | 
					                  <td class="text-center">
 | 
				
			||||||
 | 
					                    {{ formatNumber(item.messagePurchaseCan) }}
 | 
				
			||||||
 | 
					                  </td>
 | 
				
			||||||
 | 
					                  <td class="text-center">
 | 
				
			||||||
 | 
					                    {{ formatNumber(item.quotaPurchaseCan) }}
 | 
				
			||||||
 | 
					                  </td>
 | 
				
			||||||
 | 
					                  <td class="text-center font-weight-bold">
 | 
				
			||||||
 | 
					                    {{ formatNumber(item.totalCan) }}
 | 
				
			||||||
 | 
					                  </td>
 | 
				
			||||||
 | 
					                  <td class="text-center">
 | 
				
			||||||
 | 
					                    {{ formatCurrency(item.totalKrw) }}
 | 
				
			||||||
 | 
					                  </td>
 | 
				
			||||||
 | 
					                  <td class="text-center font-weight-bold">
 | 
				
			||||||
 | 
					                    {{ formatCurrency(item.settlementKrw) }}
 | 
				
			||||||
 | 
					                  </td>
 | 
				
			||||||
 | 
					                </tr>
 | 
				
			||||||
 | 
					                <tr v-if="!is_loading && items.length === 0">
 | 
				
			||||||
 | 
					                  <td
 | 
				
			||||||
 | 
					                    colspan="7"
 | 
				
			||||||
 | 
					                    class="text-center grey--text"
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                    데이터가 없습니다.
 | 
				
			||||||
 | 
					                  </td>
 | 
				
			||||||
 | 
					                </tr>
 | 
				
			||||||
 | 
					              </tbody>
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					          </v-simple-table>
 | 
				
			||||||
 | 
					        </v-col>
 | 
				
			||||||
 | 
					      </v-row>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <v-row class="text-center">
 | 
				
			||||||
 | 
					        <v-col>
 | 
				
			||||||
 | 
					          <v-pagination
 | 
				
			||||||
 | 
					            v-model="page"
 | 
				
			||||||
 | 
					            :length="total_page"
 | 
				
			||||||
 | 
					            circle
 | 
				
			||||||
 | 
					            @input="onPageChange"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </v-col>
 | 
				
			||||||
 | 
					      </v-row>
 | 
				
			||||||
 | 
					    </v-container>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
					import { getCharacterCalculateList } from "@/api/character";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function formatDate(date) {
 | 
				
			||||||
 | 
					  const pad = (n) => n.toString().padStart(2, "0");
 | 
				
			||||||
 | 
					  return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default {
 | 
				
			||||||
 | 
					  name: "CharacterCalculateList",
 | 
				
			||||||
 | 
					  data() {
 | 
				
			||||||
 | 
					    const today = new Date();
 | 
				
			||||||
 | 
					    const sevenDaysAgo = new Date();
 | 
				
			||||||
 | 
					    sevenDaysAgo.setDate(today.getDate() - 7);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      is_loading: false,
 | 
				
			||||||
 | 
					      menuStart: false,
 | 
				
			||||||
 | 
					      menuEnd: false,
 | 
				
			||||||
 | 
					      todayStr: formatDate(today),
 | 
				
			||||||
 | 
					      page: 1,
 | 
				
			||||||
 | 
					      size: 30,
 | 
				
			||||||
 | 
					      total_page: 1,
 | 
				
			||||||
 | 
					      total_count: 0,
 | 
				
			||||||
 | 
					      items: [],
 | 
				
			||||||
 | 
					      sortItems: [
 | 
				
			||||||
 | 
					        { text: "매출순", value: "TOTAL_SALES_DESC" },
 | 
				
			||||||
 | 
					        { text: "최신캐릭터순", value: "LATEST_DESC" }
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      filters: {
 | 
				
			||||||
 | 
					        startDateStr: formatDate(new Date(today.getFullYear(), today.getMonth(), 1)),
 | 
				
			||||||
 | 
					        endDateStr: formatDate(today),
 | 
				
			||||||
 | 
					        sort: "TOTAL_SALES_DESC"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  created() {
 | 
				
			||||||
 | 
					    this.fetchList(1);
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  methods: {
 | 
				
			||||||
 | 
					    notifyError(message) {
 | 
				
			||||||
 | 
					      this.$dialog && this.$dialog.notify ? this.$dialog.notify.error(message) : alert(message);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    onPageChange() {
 | 
				
			||||||
 | 
					      this.fetchList(this.page);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    resetFilters() {
 | 
				
			||||||
 | 
					      const today = new Date();
 | 
				
			||||||
 | 
					      // 이번 달 1일로 시작일 설정
 | 
				
			||||||
 | 
					      const firstDay = new Date(today.getFullYear(), today.getMonth(), 1);
 | 
				
			||||||
 | 
					      this.filters.startDateStr = formatDate(firstDay);
 | 
				
			||||||
 | 
					      // 종료일은 오늘
 | 
				
			||||||
 | 
					      this.filters.endDateStr = formatDate(today);
 | 
				
			||||||
 | 
					      this.filters.sort = "TOTAL_SALES_DESC";
 | 
				
			||||||
 | 
					      // 페이지를 1로 리셋하고 목록 조회
 | 
				
			||||||
 | 
					      this.fetchList(1);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    async fetchList(page = 1) {
 | 
				
			||||||
 | 
					      if (this.is_loading) return;
 | 
				
			||||||
 | 
					      this.is_loading = true;
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const params = {
 | 
				
			||||||
 | 
					          startDateStr: this.filters.startDateStr || null,
 | 
				
			||||||
 | 
					          endDateStr: this.filters.endDateStr || null,
 | 
				
			||||||
 | 
					          sort: this.filters.sort,
 | 
				
			||||||
 | 
					          page: (page - 1),
 | 
				
			||||||
 | 
					          size: this.size
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        const res = await getCharacterCalculateList(params);
 | 
				
			||||||
 | 
					        if (res && res.status === 200) {
 | 
				
			||||||
 | 
					          const data = res.data && res.data.data ? res.data.data : res.data;
 | 
				
			||||||
 | 
					          if (data) {
 | 
				
			||||||
 | 
					            this.total_count = data.totalCount || 0;
 | 
				
			||||||
 | 
					            this.items = data.items || [];
 | 
				
			||||||
 | 
					            const totalPage = Math.ceil(this.total_count / this.size);
 | 
				
			||||||
 | 
					            this.total_page = totalPage > 0 ? totalPage : 1;
 | 
				
			||||||
 | 
					            this.page = page;
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            this.items = [];
 | 
				
			||||||
 | 
					            this.total_count = 0;
 | 
				
			||||||
 | 
					            this.total_page = 1;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          this.notifyError("목록 조회 중 오류가 발생했습니다.");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch (e) {
 | 
				
			||||||
 | 
					        console.error("정산 목록 조회 오류:", e);
 | 
				
			||||||
 | 
					        this.notifyError("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.");
 | 
				
			||||||
 | 
					      } finally {
 | 
				
			||||||
 | 
					        this.is_loading = false;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    formatNumber(n) {
 | 
				
			||||||
 | 
					      const num = Number(n || 0);
 | 
				
			||||||
 | 
					      return num.toLocaleString("ko-KR");
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    formatCurrency(n) {
 | 
				
			||||||
 | 
					      const num = Number(n || 0);
 | 
				
			||||||
 | 
					      return num.toLocaleString("ko-KR");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style scoped>
 | 
				
			||||||
 | 
					.v-simple-table {
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										341
									
								
								src/views/Chat/CharacterCuration.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										341
									
								
								src/views/Chat/CharacterCuration.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,341 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div>
 | 
				
			||||||
 | 
					    <v-toolbar dark>
 | 
				
			||||||
 | 
					      <v-spacer />
 | 
				
			||||||
 | 
					      <v-toolbar-title>캐릭터 큐레이션</v-toolbar-title>
 | 
				
			||||||
 | 
					      <v-spacer />
 | 
				
			||||||
 | 
					    </v-toolbar>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <v-container>
 | 
				
			||||||
 | 
					      <v-row class="mb-4">
 | 
				
			||||||
 | 
					        <v-col
 | 
				
			||||||
 | 
					          cols="12"
 | 
				
			||||||
 | 
					          sm="4"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <v-btn
 | 
				
			||||||
 | 
					            color="primary"
 | 
				
			||||||
 | 
					            dark
 | 
				
			||||||
 | 
					            @click="showWriteDialog"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            큐레이션 등록
 | 
				
			||||||
 | 
					          </v-btn>
 | 
				
			||||||
 | 
					        </v-col>
 | 
				
			||||||
 | 
					      </v-row>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <v-row>
 | 
				
			||||||
 | 
					        <v-col>
 | 
				
			||||||
 | 
					          <v-data-table
 | 
				
			||||||
 | 
					            :headers="headers"
 | 
				
			||||||
 | 
					            :items="curations"
 | 
				
			||||||
 | 
					            :loading="isLoading"
 | 
				
			||||||
 | 
					            item-key="id"
 | 
				
			||||||
 | 
					            class="elevation-1"
 | 
				
			||||||
 | 
					            hide-default-footer
 | 
				
			||||||
 | 
					            disable-pagination
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <template v-slot:body="props">
 | 
				
			||||||
 | 
					              <draggable
 | 
				
			||||||
 | 
					                v-model="props.items"
 | 
				
			||||||
 | 
					                tag="tbody"
 | 
				
			||||||
 | 
					                @end="onDragEnd(props.items)"
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <tr
 | 
				
			||||||
 | 
					                  v-for="item in props.items"
 | 
				
			||||||
 | 
					                  :key="item.id"
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  <td @click="goDetail(item)">
 | 
				
			||||||
 | 
					                    {{ item.title }}
 | 
				
			||||||
 | 
					                  </td>
 | 
				
			||||||
 | 
					                  <td @click="goDetail(item)">
 | 
				
			||||||
 | 
					                    <h3 v-if="item.isAdult">
 | 
				
			||||||
 | 
					                      O
 | 
				
			||||||
 | 
					                    </h3>
 | 
				
			||||||
 | 
					                    <h3 v-else>
 | 
				
			||||||
 | 
					                      X
 | 
				
			||||||
 | 
					                    </h3>
 | 
				
			||||||
 | 
					                  </td>
 | 
				
			||||||
 | 
					                  <td>
 | 
				
			||||||
 | 
					                    <v-row>
 | 
				
			||||||
 | 
					                      <v-col class="text-center">
 | 
				
			||||||
 | 
					                        <v-btn
 | 
				
			||||||
 | 
					                          small
 | 
				
			||||||
 | 
					                          color="primary"
 | 
				
			||||||
 | 
					                          :disabled="isLoading"
 | 
				
			||||||
 | 
					                          @click="showModifyDialog(item)"
 | 
				
			||||||
 | 
					                        >
 | 
				
			||||||
 | 
					                          수정
 | 
				
			||||||
 | 
					                        </v-btn>
 | 
				
			||||||
 | 
					                      </v-col>
 | 
				
			||||||
 | 
					                      <v-col class="text-center">
 | 
				
			||||||
 | 
					                        <v-btn
 | 
				
			||||||
 | 
					                          small
 | 
				
			||||||
 | 
					                          color="error"
 | 
				
			||||||
 | 
					                          :disabled="isLoading"
 | 
				
			||||||
 | 
					                          @click="confirmDelete(item)"
 | 
				
			||||||
 | 
					                        >
 | 
				
			||||||
 | 
					                          삭제
 | 
				
			||||||
 | 
					                        </v-btn>
 | 
				
			||||||
 | 
					                      </v-col>
 | 
				
			||||||
 | 
					                    </v-row>
 | 
				
			||||||
 | 
					                  </td>
 | 
				
			||||||
 | 
					                </tr>
 | 
				
			||||||
 | 
					              </draggable>
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					          </v-data-table>
 | 
				
			||||||
 | 
					        </v-col>
 | 
				
			||||||
 | 
					      </v-row>
 | 
				
			||||||
 | 
					    </v-container>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <!-- 등록/수정 다이얼로그 -->
 | 
				
			||||||
 | 
					    <v-dialog
 | 
				
			||||||
 | 
					      v-model="showDialog"
 | 
				
			||||||
 | 
					      max-width="600px"
 | 
				
			||||||
 | 
					      persistent
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <v-card>
 | 
				
			||||||
 | 
					        <v-card-title>
 | 
				
			||||||
 | 
					          <span class="headline">{{ isModify ? '큐레이션 수정' : '큐레이션 등록' }}</span>
 | 
				
			||||||
 | 
					        </v-card-title>
 | 
				
			||||||
 | 
					        <v-card-text>
 | 
				
			||||||
 | 
					          <v-container>
 | 
				
			||||||
 | 
					            <v-row>
 | 
				
			||||||
 | 
					              <v-col cols="12">
 | 
				
			||||||
 | 
					                <v-text-field
 | 
				
			||||||
 | 
					                  v-model="form.title"
 | 
				
			||||||
 | 
					                  label="제목"
 | 
				
			||||||
 | 
					                  outlined
 | 
				
			||||||
 | 
					                  required
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              </v-col>
 | 
				
			||||||
 | 
					            </v-row>
 | 
				
			||||||
 | 
					            <v-row>
 | 
				
			||||||
 | 
					              <v-col cols="12">
 | 
				
			||||||
 | 
					                <v-checkbox
 | 
				
			||||||
 | 
					                  v-model="form.isAdult"
 | 
				
			||||||
 | 
					                  label="19금"
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              </v-col>
 | 
				
			||||||
 | 
					            </v-row>
 | 
				
			||||||
 | 
					          </v-container>
 | 
				
			||||||
 | 
					        </v-card-text>
 | 
				
			||||||
 | 
					        <v-card-actions>
 | 
				
			||||||
 | 
					          <v-spacer />
 | 
				
			||||||
 | 
					          <v-btn
 | 
				
			||||||
 | 
					            text
 | 
				
			||||||
 | 
					            color="blue darken-1"
 | 
				
			||||||
 | 
					            @click="closeDialog"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            취소
 | 
				
			||||||
 | 
					          </v-btn>
 | 
				
			||||||
 | 
					          <v-btn
 | 
				
			||||||
 | 
					            text
 | 
				
			||||||
 | 
					            color="blue darken-1"
 | 
				
			||||||
 | 
					            :loading="isSubmitting"
 | 
				
			||||||
 | 
					            :disabled="!isFormValid || isSubmitting"
 | 
				
			||||||
 | 
					            @click="saveCuration"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            저장
 | 
				
			||||||
 | 
					          </v-btn>
 | 
				
			||||||
 | 
					        </v-card-actions>
 | 
				
			||||||
 | 
					      </v-card>
 | 
				
			||||||
 | 
					    </v-dialog>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <!-- 삭제 확인 다이얼로그 -->
 | 
				
			||||||
 | 
					    <v-dialog
 | 
				
			||||||
 | 
					      v-model="showDeleteDialog"
 | 
				
			||||||
 | 
					      max-width="400px"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <v-card>
 | 
				
			||||||
 | 
					        <v-card-title class="headline">
 | 
				
			||||||
 | 
					          큐레이션 삭제
 | 
				
			||||||
 | 
					        </v-card-title>
 | 
				
			||||||
 | 
					        <v-card-text>"{{ selectedCuration && selectedCuration.title }}"을(를) 삭제하시겠습니까?</v-card-text>
 | 
				
			||||||
 | 
					        <v-card-actions>
 | 
				
			||||||
 | 
					          <v-spacer />
 | 
				
			||||||
 | 
					          <v-btn
 | 
				
			||||||
 | 
					            text
 | 
				
			||||||
 | 
					            color="blue darken-1"
 | 
				
			||||||
 | 
					            @click="showDeleteDialog = false"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            취소
 | 
				
			||||||
 | 
					          </v-btn>
 | 
				
			||||||
 | 
					          <v-btn
 | 
				
			||||||
 | 
					            text
 | 
				
			||||||
 | 
					            color="red darken-1"
 | 
				
			||||||
 | 
					            :loading="isSubmitting"
 | 
				
			||||||
 | 
					            @click="deleteCuration"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            삭제
 | 
				
			||||||
 | 
					          </v-btn>
 | 
				
			||||||
 | 
					        </v-card-actions>
 | 
				
			||||||
 | 
					      </v-card>
 | 
				
			||||||
 | 
					    </v-dialog>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
					import draggable from 'vuedraggable';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  getCharacterCurationList,
 | 
				
			||||||
 | 
					  createCharacterCuration,
 | 
				
			||||||
 | 
					  updateCharacterCuration,
 | 
				
			||||||
 | 
					  deleteCharacterCuration,
 | 
				
			||||||
 | 
					  updateCharacterCurationOrder
 | 
				
			||||||
 | 
					} from '@/api/character';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default {
 | 
				
			||||||
 | 
					  name: 'CharacterCuration',
 | 
				
			||||||
 | 
					  components: { draggable },
 | 
				
			||||||
 | 
					  data() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      isLoading: false,
 | 
				
			||||||
 | 
					      isSubmitting: false,
 | 
				
			||||||
 | 
					      curations: [],
 | 
				
			||||||
 | 
					      headers: [
 | 
				
			||||||
 | 
					        { text: '제목', align: 'center', sortable: false, value: 'title' },
 | 
				
			||||||
 | 
					        { text: '19금', align: 'center', sortable: false, value: 'isAdult' },
 | 
				
			||||||
 | 
					        { text: '관리', align: 'center', sortable: false, value: 'management' }
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      showDialog: false,
 | 
				
			||||||
 | 
					      isModify: false,
 | 
				
			||||||
 | 
					      form: { id: null, title: '', isAdult: false },
 | 
				
			||||||
 | 
					      selectedCuration: null,
 | 
				
			||||||
 | 
					      showDeleteDialog: false
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  computed: {
 | 
				
			||||||
 | 
					    isFormValid() {
 | 
				
			||||||
 | 
					      return this.form.title && this.form.title.trim().length > 0;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  async created() {
 | 
				
			||||||
 | 
					    await this.loadCurations();
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  methods: {
 | 
				
			||||||
 | 
					    notifyError(message) { this.$dialog.notify.error(message); },
 | 
				
			||||||
 | 
					    notifySuccess(message) { this.$dialog.notify.success(message); },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async loadCurations() {
 | 
				
			||||||
 | 
					      this.isLoading = true;
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const res = await getCharacterCurationList();
 | 
				
			||||||
 | 
					        if (res.status === 200 && res.data && res.data.success === true) {
 | 
				
			||||||
 | 
					          this.curations = res.data.data || [];
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          this.notifyError(res.data.message || '목록을 불러오지 못했습니다.');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch (e) {
 | 
				
			||||||
 | 
					        this.notifyError('목록을 불러오지 못했습니다.');
 | 
				
			||||||
 | 
					      } finally {
 | 
				
			||||||
 | 
					        this.isLoading = false;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    onDragEnd(items) {
 | 
				
			||||||
 | 
					      const ids = items.map(i => i.id);
 | 
				
			||||||
 | 
					      this.updateOrders(ids);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async updateOrders(ids) {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const res = await updateCharacterCurationOrder(ids);
 | 
				
			||||||
 | 
					        if (res.status === 200 && res.data && res.data.success === true) {
 | 
				
			||||||
 | 
					          this.notifySuccess('순서가 변경되었습니다.');
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          this.notifyError(res.data.message || '순서 변경에 실패했습니다.');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch (e) {
 | 
				
			||||||
 | 
					        this.notifyError('순서 변경에 실패했습니다.');
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    goDetail(item) {
 | 
				
			||||||
 | 
					      this.$router.push({
 | 
				
			||||||
 | 
					        name: 'CharacterCurationDetail',
 | 
				
			||||||
 | 
					        params: { curationId: item.id, title: item.title, isAdult: item.isAdult }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    showWriteDialog() {
 | 
				
			||||||
 | 
					      this.isModify = false;
 | 
				
			||||||
 | 
					      this.form = { id: null, title: '', isAdult: false };
 | 
				
			||||||
 | 
					      this.showDialog = true;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    showModifyDialog(item) {
 | 
				
			||||||
 | 
					      this.isModify = true;
 | 
				
			||||||
 | 
					      this.form = { id: item.id, title: item.title, isAdult: item.isAdult };
 | 
				
			||||||
 | 
					      this.showDialog = true;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    closeDialog() {
 | 
				
			||||||
 | 
					      this.showDialog = false;
 | 
				
			||||||
 | 
					      this.form = { id: null, title: '', isAdult: false };
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async saveCuration() {
 | 
				
			||||||
 | 
					      if (this.isSubmitting || !this.isFormValid) return;
 | 
				
			||||||
 | 
					      this.isSubmitting = true;
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        if (this.isModify) {
 | 
				
			||||||
 | 
					          const payload = { id: this.form.id };
 | 
				
			||||||
 | 
					          if (this.form.title) payload.title = this.form.title;
 | 
				
			||||||
 | 
					          payload.isAdult = this.form.isAdult;
 | 
				
			||||||
 | 
					          const res = await updateCharacterCuration(payload);
 | 
				
			||||||
 | 
					          if (res.status === 200 && res.data && res.data.success === true) {
 | 
				
			||||||
 | 
					            this.notifySuccess('수정되었습니다.');
 | 
				
			||||||
 | 
					            this.closeDialog();
 | 
				
			||||||
 | 
					            await this.loadCurations();
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            this.notifyError(res.data.message || '수정에 실패했습니다.');
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          const res = await createCharacterCuration({
 | 
				
			||||||
 | 
					            title: this.form.title,
 | 
				
			||||||
 | 
					            isAdult: this.form.isAdult,
 | 
				
			||||||
 | 
					            isActive: true
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					          if (res.status === 200 && res.data && res.data.success === true) {
 | 
				
			||||||
 | 
					            this.notifySuccess('등록되었습니다.');
 | 
				
			||||||
 | 
					            this.closeDialog();
 | 
				
			||||||
 | 
					            await this.loadCurations();
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            this.notifyError(res.data.message || '등록에 실패했습니다.');
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch (e) {
 | 
				
			||||||
 | 
					        this.notifyError('저장 중 오류가 발생했습니다.');
 | 
				
			||||||
 | 
					      } finally {
 | 
				
			||||||
 | 
					        this.isSubmitting = false;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    confirmDelete(item) {
 | 
				
			||||||
 | 
					      this.selectedCuration = item;
 | 
				
			||||||
 | 
					      this.showDeleteDialog = true;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async deleteCuration() {
 | 
				
			||||||
 | 
					      if (!this.selectedCuration) return;
 | 
				
			||||||
 | 
					      this.isSubmitting = true;
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const res = await deleteCharacterCuration(this.selectedCuration.id);
 | 
				
			||||||
 | 
					        if (res.status === 200 && res.data && res.data.success === true) {
 | 
				
			||||||
 | 
					          this.notifySuccess('삭제되었습니다.');
 | 
				
			||||||
 | 
					          this.showDeleteDialog = false;
 | 
				
			||||||
 | 
					          await this.loadCurations();
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          this.notifyError(res.data.message || '삭제에 실패했습니다.');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch (e) {
 | 
				
			||||||
 | 
					        this.notifyError('삭제에 실패했습니다.');
 | 
				
			||||||
 | 
					      } finally {
 | 
				
			||||||
 | 
					        this.isSubmitting = false;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style scoped>
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										429
									
								
								src/views/Chat/CharacterCurationDetail.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										429
									
								
								src/views/Chat/CharacterCurationDetail.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,429 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div>
 | 
				
			||||||
 | 
					    <v-toolbar dark>
 | 
				
			||||||
 | 
					      <v-btn
 | 
				
			||||||
 | 
					        icon
 | 
				
			||||||
 | 
					        @click="goBack"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <v-icon>mdi-arrow-left</v-icon>
 | 
				
			||||||
 | 
					      </v-btn>
 | 
				
			||||||
 | 
					      <v-spacer />
 | 
				
			||||||
 | 
					      <v-toolbar-title>{{ title }}</v-toolbar-title>
 | 
				
			||||||
 | 
					      <v-spacer />
 | 
				
			||||||
 | 
					    </v-toolbar>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <v-container>
 | 
				
			||||||
 | 
					      <v-row class="mb-2">
 | 
				
			||||||
 | 
					        <v-col
 | 
				
			||||||
 | 
					          cols="4"
 | 
				
			||||||
 | 
					          class="text-right"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          19금 :
 | 
				
			||||||
 | 
					        </v-col>
 | 
				
			||||||
 | 
					        <v-col cols="8">
 | 
				
			||||||
 | 
					          {{ isAdult ? 'O' : 'X' }}
 | 
				
			||||||
 | 
					        </v-col>
 | 
				
			||||||
 | 
					      </v-row>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <v-row class="mb-4">
 | 
				
			||||||
 | 
					        <v-col
 | 
				
			||||||
 | 
					          cols="12"
 | 
				
			||||||
 | 
					          sm="4"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <v-btn
 | 
				
			||||||
 | 
					            color="primary"
 | 
				
			||||||
 | 
					            dark
 | 
				
			||||||
 | 
					            @click="openAddDialog"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            캐릭터 등록
 | 
				
			||||||
 | 
					          </v-btn>
 | 
				
			||||||
 | 
					        </v-col>
 | 
				
			||||||
 | 
					      </v-row>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <v-row>
 | 
				
			||||||
 | 
					        <draggable
 | 
				
			||||||
 | 
					          v-model="characters"
 | 
				
			||||||
 | 
					          class="row"
 | 
				
			||||||
 | 
					          style="width: 100%"
 | 
				
			||||||
 | 
					          :options="{ animation: 150 }"
 | 
				
			||||||
 | 
					          @end="onDragEnd"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <v-col
 | 
				
			||||||
 | 
					            v-for="ch in characters"
 | 
				
			||||||
 | 
					            :key="ch.id"
 | 
				
			||||||
 | 
					            cols="12"
 | 
				
			||||||
 | 
					            sm="6"
 | 
				
			||||||
 | 
					            md="4"
 | 
				
			||||||
 | 
					            lg="3"
 | 
				
			||||||
 | 
					            class="mb-4"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <v-card>
 | 
				
			||||||
 | 
					              <v-img
 | 
				
			||||||
 | 
					                :src="ch.imageUrl"
 | 
				
			||||||
 | 
					                height="200"
 | 
				
			||||||
 | 
					                contain
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					              <v-card-text class="text-center">
 | 
				
			||||||
 | 
					                {{ ch.name }}
 | 
				
			||||||
 | 
					              </v-card-text>
 | 
				
			||||||
 | 
					              <v-card-text class="text-center">
 | 
				
			||||||
 | 
					                {{ ch.description }}
 | 
				
			||||||
 | 
					              </v-card-text>
 | 
				
			||||||
 | 
					              <v-card-actions>
 | 
				
			||||||
 | 
					                <v-spacer />
 | 
				
			||||||
 | 
					                <v-btn
 | 
				
			||||||
 | 
					                  small
 | 
				
			||||||
 | 
					                  color="error"
 | 
				
			||||||
 | 
					                  @click="confirmRemove(ch)"
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  삭제
 | 
				
			||||||
 | 
					                </v-btn>
 | 
				
			||||||
 | 
					                <v-spacer />
 | 
				
			||||||
 | 
					              </v-card-actions>
 | 
				
			||||||
 | 
					            </v-card>
 | 
				
			||||||
 | 
					          </v-col>
 | 
				
			||||||
 | 
					        </draggable>
 | 
				
			||||||
 | 
					      </v-row>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <v-row v-if="isLoading && characters.length === 0">
 | 
				
			||||||
 | 
					        <v-col class="text-center">
 | 
				
			||||||
 | 
					          <v-progress-circular
 | 
				
			||||||
 | 
					            indeterminate
 | 
				
			||||||
 | 
					            color="primary"
 | 
				
			||||||
 | 
					            size="48"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </v-col>
 | 
				
			||||||
 | 
					      </v-row>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <v-row v-if="!isLoading && characters.length === 0">
 | 
				
			||||||
 | 
					        <v-col class="text-center">
 | 
				
			||||||
 | 
					          등록된 캐릭터가 없습니다.
 | 
				
			||||||
 | 
					        </v-col>
 | 
				
			||||||
 | 
					      </v-row>
 | 
				
			||||||
 | 
					    </v-container>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <!-- 등록 다이얼로그 -->
 | 
				
			||||||
 | 
					    <v-dialog
 | 
				
			||||||
 | 
					      v-model="showAddDialog"
 | 
				
			||||||
 | 
					      max-width="700px"
 | 
				
			||||||
 | 
					      persistent
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <v-card>
 | 
				
			||||||
 | 
					        <v-card-title>캐릭터 등록</v-card-title>
 | 
				
			||||||
 | 
					        <v-card-text>
 | 
				
			||||||
 | 
					          <v-text-field
 | 
				
			||||||
 | 
					            v-model="searchWord"
 | 
				
			||||||
 | 
					            label="캐릭터 검색"
 | 
				
			||||||
 | 
					            outlined
 | 
				
			||||||
 | 
					            @keyup.enter="search"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					          <v-btn
 | 
				
			||||||
 | 
					            color="primary"
 | 
				
			||||||
 | 
					            small
 | 
				
			||||||
 | 
					            class="mb-2"
 | 
				
			||||||
 | 
					            @click="search"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            검색
 | 
				
			||||||
 | 
					          </v-btn>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <v-row v-if="searchResults.length > 0 || addList.length > 0">
 | 
				
			||||||
 | 
					            <v-col>
 | 
				
			||||||
 | 
					              검색결과
 | 
				
			||||||
 | 
					              <v-simple-table>
 | 
				
			||||||
 | 
					                <template v-slot:default>
 | 
				
			||||||
 | 
					                  <thead>
 | 
				
			||||||
 | 
					                    <tr>
 | 
				
			||||||
 | 
					                      <th class="text-center">
 | 
				
			||||||
 | 
					                        이름
 | 
				
			||||||
 | 
					                      </th>
 | 
				
			||||||
 | 
					                      <th />
 | 
				
			||||||
 | 
					                    </tr>
 | 
				
			||||||
 | 
					                  </thead>
 | 
				
			||||||
 | 
					                  <tbody>
 | 
				
			||||||
 | 
					                    <tr
 | 
				
			||||||
 | 
					                      v-for="item in searchResults"
 | 
				
			||||||
 | 
					                      :key="item.id"
 | 
				
			||||||
 | 
					                    >
 | 
				
			||||||
 | 
					                      <td>{{ item.name }}</td>
 | 
				
			||||||
 | 
					                      <td>
 | 
				
			||||||
 | 
					                        <v-btn
 | 
				
			||||||
 | 
					                          small
 | 
				
			||||||
 | 
					                          color="primary"
 | 
				
			||||||
 | 
					                          @click="addItem(item)"
 | 
				
			||||||
 | 
					                        >
 | 
				
			||||||
 | 
					                          추가
 | 
				
			||||||
 | 
					                        </v-btn>
 | 
				
			||||||
 | 
					                      </td>
 | 
				
			||||||
 | 
					                    </tr>
 | 
				
			||||||
 | 
					                  </tbody>
 | 
				
			||||||
 | 
					                </template>
 | 
				
			||||||
 | 
					              </v-simple-table>
 | 
				
			||||||
 | 
					            </v-col>
 | 
				
			||||||
 | 
					            <v-col v-if="addList.length > 0">
 | 
				
			||||||
 | 
					              추가할 캐릭터
 | 
				
			||||||
 | 
					              <v-simple-table>
 | 
				
			||||||
 | 
					                <template>
 | 
				
			||||||
 | 
					                  <thead>
 | 
				
			||||||
 | 
					                    <tr>
 | 
				
			||||||
 | 
					                      <th class="text-center">
 | 
				
			||||||
 | 
					                        이름
 | 
				
			||||||
 | 
					                      </th>
 | 
				
			||||||
 | 
					                      <th />
 | 
				
			||||||
 | 
					                    </tr>
 | 
				
			||||||
 | 
					                  </thead>
 | 
				
			||||||
 | 
					                  <tbody>
 | 
				
			||||||
 | 
					                    <tr
 | 
				
			||||||
 | 
					                      v-for="item in addList"
 | 
				
			||||||
 | 
					                      :key="item.id"
 | 
				
			||||||
 | 
					                    >
 | 
				
			||||||
 | 
					                      <td>{{ item.name }}</td>
 | 
				
			||||||
 | 
					                      <td>
 | 
				
			||||||
 | 
					                        <v-btn
 | 
				
			||||||
 | 
					                          small
 | 
				
			||||||
 | 
					                          color="error"
 | 
				
			||||||
 | 
					                          @click="removeItem(item)"
 | 
				
			||||||
 | 
					                        >
 | 
				
			||||||
 | 
					                          제거
 | 
				
			||||||
 | 
					                        </v-btn>
 | 
				
			||||||
 | 
					                      </td>
 | 
				
			||||||
 | 
					                    </tr>
 | 
				
			||||||
 | 
					                  </tbody>
 | 
				
			||||||
 | 
					                </template>
 | 
				
			||||||
 | 
					              </v-simple-table>
 | 
				
			||||||
 | 
					            </v-col>
 | 
				
			||||||
 | 
					          </v-row>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <v-alert
 | 
				
			||||||
 | 
					            v-else-if="searchPerformed"
 | 
				
			||||||
 | 
					            type="info"
 | 
				
			||||||
 | 
					            outlined
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            검색결과가 없습니다.
 | 
				
			||||||
 | 
					          </v-alert>
 | 
				
			||||||
 | 
					        </v-card-text>
 | 
				
			||||||
 | 
					        <v-card-actions>
 | 
				
			||||||
 | 
					          <v-spacer />
 | 
				
			||||||
 | 
					          <v-btn
 | 
				
			||||||
 | 
					            text
 | 
				
			||||||
 | 
					            color="blue darken-1"
 | 
				
			||||||
 | 
					            @click="closeAddDialog"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            취소
 | 
				
			||||||
 | 
					          </v-btn>
 | 
				
			||||||
 | 
					          <v-btn
 | 
				
			||||||
 | 
					            text
 | 
				
			||||||
 | 
					            color="blue darken-1"
 | 
				
			||||||
 | 
					            :disabled="addList.length === 0 || isSubmitting"
 | 
				
			||||||
 | 
					            :loading="isSubmitting"
 | 
				
			||||||
 | 
					            @click="addItemInCuration"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            추가
 | 
				
			||||||
 | 
					          </v-btn>
 | 
				
			||||||
 | 
					        </v-card-actions>
 | 
				
			||||||
 | 
					      </v-card>
 | 
				
			||||||
 | 
					    </v-dialog>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <!-- 삭제 확인 다이얼로그 -->
 | 
				
			||||||
 | 
					    <v-dialog
 | 
				
			||||||
 | 
					      v-model="showDeleteDialog"
 | 
				
			||||||
 | 
					      max-width="420px"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <v-card>
 | 
				
			||||||
 | 
					        <v-card-title class="headline">
 | 
				
			||||||
 | 
					          캐릭터 삭제
 | 
				
			||||||
 | 
					        </v-card-title>
 | 
				
			||||||
 | 
					        <v-card-text>"{{ targetCharacter && targetCharacter.name }}"을(를) 큐레이션에서 삭제할까요?</v-card-text>
 | 
				
			||||||
 | 
					        <v-card-actions>
 | 
				
			||||||
 | 
					          <v-spacer />
 | 
				
			||||||
 | 
					          <v-btn
 | 
				
			||||||
 | 
					            text
 | 
				
			||||||
 | 
					            color="blue darken-1"
 | 
				
			||||||
 | 
					            @click="showDeleteDialog = false"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            취소
 | 
				
			||||||
 | 
					          </v-btn>
 | 
				
			||||||
 | 
					          <v-btn
 | 
				
			||||||
 | 
					            text
 | 
				
			||||||
 | 
					            color="red darken-1"
 | 
				
			||||||
 | 
					            :loading="isSubmitting"
 | 
				
			||||||
 | 
					            @click="removeTarget"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            삭제
 | 
				
			||||||
 | 
					          </v-btn>
 | 
				
			||||||
 | 
					        </v-card-actions>
 | 
				
			||||||
 | 
					      </v-card>
 | 
				
			||||||
 | 
					    </v-dialog>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
					import draggable from 'vuedraggable';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  getCharactersInCuration,
 | 
				
			||||||
 | 
					  addCharacterToCuration,
 | 
				
			||||||
 | 
					  removeCharacterFromCuration,
 | 
				
			||||||
 | 
					  updateCurationCharactersOrder,
 | 
				
			||||||
 | 
					  searchCharacters
 | 
				
			||||||
 | 
					} from '@/api/character';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default {
 | 
				
			||||||
 | 
					  name: 'CharacterCurationDetail',
 | 
				
			||||||
 | 
					  components: { draggable },
 | 
				
			||||||
 | 
					  data() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      isLoading: false,
 | 
				
			||||||
 | 
					      isSubmitting: false,
 | 
				
			||||||
 | 
					      curationId: null,
 | 
				
			||||||
 | 
					      title: '',
 | 
				
			||||||
 | 
					      isAdult: false,
 | 
				
			||||||
 | 
					      characters: [],
 | 
				
			||||||
 | 
					      showAddDialog: false,
 | 
				
			||||||
 | 
					      showDeleteDialog: false,
 | 
				
			||||||
 | 
					      targetCharacter: null,
 | 
				
			||||||
 | 
					      searchWord: '',
 | 
				
			||||||
 | 
					      searchResults: [],
 | 
				
			||||||
 | 
					      searchPerformed: false,
 | 
				
			||||||
 | 
					      addList: []
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  async created() {
 | 
				
			||||||
 | 
					    this.curationId = this.$route.params.curationId;
 | 
				
			||||||
 | 
					    this.title = this.$route.params.title;
 | 
				
			||||||
 | 
					    this.isAdult = this.$route.params.isAdult;
 | 
				
			||||||
 | 
					    await this.loadCharacters();
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  methods: {
 | 
				
			||||||
 | 
					    notifyError(message) { this.$dialog.notify.error(message); },
 | 
				
			||||||
 | 
					    notifySuccess(message) { this.$dialog.notify.success(message); },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    goBack() { this.$router.push({ name: 'CharacterCuration' }); },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async loadCharacters() {
 | 
				
			||||||
 | 
					      this.isLoading = true;
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const res = await getCharactersInCuration(this.curationId);
 | 
				
			||||||
 | 
					        if (res.status === 200 && res.data && res.data.success === true) {
 | 
				
			||||||
 | 
					          this.characters = res.data.data || [];
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          this.notifyError(res.data.message || '캐릭터 목록을 불러오지 못했습니다.');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch (e) {
 | 
				
			||||||
 | 
					        this.notifyError('캐릭터 목록을 불러오지 못했습니다.');
 | 
				
			||||||
 | 
					      } finally {
 | 
				
			||||||
 | 
					        this.isLoading = false;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    openAddDialog() {
 | 
				
			||||||
 | 
					      this.showAddDialog = true;
 | 
				
			||||||
 | 
					      this.searchWord = '';
 | 
				
			||||||
 | 
					      this.searchResults = [];
 | 
				
			||||||
 | 
					      this.addList = [];
 | 
				
			||||||
 | 
					      this.searchPerformed = false;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    closeAddDialog() {
 | 
				
			||||||
 | 
					      this.showAddDialog = false;
 | 
				
			||||||
 | 
					      this.searchWord = '';
 | 
				
			||||||
 | 
					      this.searchResults = [];
 | 
				
			||||||
 | 
					      this.addList = [];
 | 
				
			||||||
 | 
					      this.searchPerformed = false;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async search() {
 | 
				
			||||||
 | 
					      if (!this.searchWord || this.searchWord.length < 2) {
 | 
				
			||||||
 | 
					        this.notifyError('검색어를 2글자 이상 입력하세요.');
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const res = await searchCharacters(this.searchWord);
 | 
				
			||||||
 | 
					        if (res.status === 200 && res.data && res.data.success === true) {
 | 
				
			||||||
 | 
					          const data = res.data.data;
 | 
				
			||||||
 | 
					          const list = data.content || [];
 | 
				
			||||||
 | 
					          const existingIds = new Set(this.characters.map(c => c.id));
 | 
				
			||||||
 | 
					          const pendingIds = new Set(this.addList.map(c => c.id));
 | 
				
			||||||
 | 
					          this.searchResults = list.filter(item => !existingIds.has(item.id) && !pendingIds.has(item.id));
 | 
				
			||||||
 | 
					          this.searchPerformed = true;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          this.notifyError(res.data.message || '검색에 실패했습니다.');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch (e) {
 | 
				
			||||||
 | 
					        this.notifyError('검색에 실패했습니다.');
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    addItem(item) {
 | 
				
			||||||
 | 
					      // 검색결과에서 제거하고 추가 목록에 삽입 (중복 방지)
 | 
				
			||||||
 | 
					      if (!this.addList.find(t => t.id === item.id)) {
 | 
				
			||||||
 | 
					        this.addList.push(item);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      this.searchResults = this.searchResults.filter(t => t.id !== item.id);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    removeItem(item) {
 | 
				
			||||||
 | 
					      this.addList = this.addList.filter(t => t.id !== item.id);
 | 
				
			||||||
 | 
					      // 제거 시 검색결과에 다시 추가
 | 
				
			||||||
 | 
					      if (!this.searchResults.find(t => t.id === item.id)) {
 | 
				
			||||||
 | 
					        this.searchResults.push(item);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async addItemInCuration() {
 | 
				
			||||||
 | 
					      if (!this.addList || this.addList.length === 0) return;
 | 
				
			||||||
 | 
					      this.isSubmitting = true;
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const ids = this.addList.map(i => i.id);
 | 
				
			||||||
 | 
					        const res = await addCharacterToCuration(this.curationId, ids);
 | 
				
			||||||
 | 
					        if (res.status === 200 && res.data && res.data.success === true) {
 | 
				
			||||||
 | 
					          this.notifySuccess(`${this.addList.length}명 추가되었습니다.`);
 | 
				
			||||||
 | 
					          this.closeAddDialog();
 | 
				
			||||||
 | 
					          await this.loadCharacters();
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          this.notifyError((res.data && res.data.message) || '추가에 실패했습니다.');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch (e) {
 | 
				
			||||||
 | 
					        this.notifyError('추가에 실패했습니다.');
 | 
				
			||||||
 | 
					      } finally {
 | 
				
			||||||
 | 
					        this.isSubmitting = false;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    confirmRemove(item) { this.targetCharacter = item; this.showDeleteDialog = true; },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async removeTarget() {
 | 
				
			||||||
 | 
					      if (!this.targetCharacter) return;
 | 
				
			||||||
 | 
					      this.isSubmitting = true;
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const res = await removeCharacterFromCuration(this.curationId, this.targetCharacter.id);
 | 
				
			||||||
 | 
					        if (res.status === 200 && res.data && res.data.success === true) {
 | 
				
			||||||
 | 
					          this.notifySuccess('삭제되었습니다.');
 | 
				
			||||||
 | 
					          this.showDeleteDialog = false;
 | 
				
			||||||
 | 
					          await this.loadCharacters();
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          this.notifyError(res.data.message || '삭제에 실패했습니다.');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch (e) {
 | 
				
			||||||
 | 
					        this.notifyError('삭제에 실패했습니다.');
 | 
				
			||||||
 | 
					      } finally {
 | 
				
			||||||
 | 
					        this.isSubmitting = false;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async onDragEnd() {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const ids = this.characters.map(c => c.id);
 | 
				
			||||||
 | 
					        const res = await updateCurationCharactersOrder(this.curationId, ids);
 | 
				
			||||||
 | 
					        if (res.status === 200 && res.data && res.data.success === true) {
 | 
				
			||||||
 | 
					          this.notifySuccess('순서가 변경되었습니다.');
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          this.notifyError(res.data.message || '순서 변경에 실패했습니다.');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch (e) {
 | 
				
			||||||
 | 
					        this.notifyError('순서 변경에 실패했습니다.');
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style scoped>
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										1968
									
								
								src/views/Chat/CharacterForm.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1968
									
								
								src/views/Chat/CharacterForm.vue
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										306
									
								
								src/views/Chat/CharacterImageForm.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										306
									
								
								src/views/Chat/CharacterImageForm.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,306 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div>
 | 
				
			||||||
 | 
					    <v-toolbar dark>
 | 
				
			||||||
 | 
					      <v-btn
 | 
				
			||||||
 | 
					        icon
 | 
				
			||||||
 | 
					        @click="goBack"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <v-icon>mdi-arrow-left</v-icon>
 | 
				
			||||||
 | 
					      </v-btn>
 | 
				
			||||||
 | 
					      <v-spacer />
 | 
				
			||||||
 | 
					      <v-toolbar-title>{{ isEdit ? '이미지 수정' : '이미지 등록' }}</v-toolbar-title>
 | 
				
			||||||
 | 
					      <v-spacer />
 | 
				
			||||||
 | 
					    </v-toolbar>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <v-container>
 | 
				
			||||||
 | 
					      <v-card class="pa-4">
 | 
				
			||||||
 | 
					        <v-form
 | 
				
			||||||
 | 
					          ref="form"
 | 
				
			||||||
 | 
					          v-model="isFormValid"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <v-card-text>
 | 
				
			||||||
 | 
					            <v-row>
 | 
				
			||||||
 | 
					              <v-col
 | 
				
			||||||
 | 
					                v-show="!isEdit"
 | 
				
			||||||
 | 
					                cols="12"
 | 
				
			||||||
 | 
					                md="6"
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <v-file-input
 | 
				
			||||||
 | 
					                  v-if="!isEdit"
 | 
				
			||||||
 | 
					                  v-model="form.image"
 | 
				
			||||||
 | 
					                  label="이미지 (800x1000 비율 권장)"
 | 
				
			||||||
 | 
					                  accept="image/*"
 | 
				
			||||||
 | 
					                  prepend-icon="mdi-camera"
 | 
				
			||||||
 | 
					                  show-size
 | 
				
			||||||
 | 
					                  truncate-length="15"
 | 
				
			||||||
 | 
					                  outlined
 | 
				
			||||||
 | 
					                  dense
 | 
				
			||||||
 | 
					                  :rules="imageRules"
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              </v-col>
 | 
				
			||||||
 | 
					              <v-col
 | 
				
			||||||
 | 
					                v-if="previewImage || form.imageUrl"
 | 
				
			||||||
 | 
					                cols="12"
 | 
				
			||||||
 | 
					                :md="isEdit ? 12 : 6"
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <div class="text-center">
 | 
				
			||||||
 | 
					                  <v-img
 | 
				
			||||||
 | 
					                    :src="previewImage || form.imageUrl"
 | 
				
			||||||
 | 
					                    max-height="240"
 | 
				
			||||||
 | 
					                    :aspect-ratio="0.8"
 | 
				
			||||||
 | 
					                    contain
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					              </v-col>
 | 
				
			||||||
 | 
					            </v-row>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <v-row>
 | 
				
			||||||
 | 
					              <v-col
 | 
				
			||||||
 | 
					                cols="12"
 | 
				
			||||||
 | 
					                md="6"
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <v-text-field
 | 
				
			||||||
 | 
					                  v-model.number="form.soloPurchasePriceCan"
 | 
				
			||||||
 | 
					                  label="이미지 단독 구매 가격(캔)"
 | 
				
			||||||
 | 
					                  type="number"
 | 
				
			||||||
 | 
					                  min="0"
 | 
				
			||||||
 | 
					                  outlined
 | 
				
			||||||
 | 
					                  dense
 | 
				
			||||||
 | 
					                  :disabled="isEdit"
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              </v-col>
 | 
				
			||||||
 | 
					              <v-col
 | 
				
			||||||
 | 
					                cols="12"
 | 
				
			||||||
 | 
					                md="6"
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <v-text-field
 | 
				
			||||||
 | 
					                  v-model.number="form.messagePurchasePriceCan"
 | 
				
			||||||
 | 
					                  label="메시지에서 구매 가격(캔)"
 | 
				
			||||||
 | 
					                  type="number"
 | 
				
			||||||
 | 
					                  min="0"
 | 
				
			||||||
 | 
					                  outlined
 | 
				
			||||||
 | 
					                  dense
 | 
				
			||||||
 | 
					                  :disabled="isEdit"
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              </v-col>
 | 
				
			||||||
 | 
					            </v-row>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <v-row>
 | 
				
			||||||
 | 
					              <v-col
 | 
				
			||||||
 | 
					                cols="12"
 | 
				
			||||||
 | 
					                md="6"
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <v-switch
 | 
				
			||||||
 | 
					                  v-model="form.adult"
 | 
				
			||||||
 | 
					                  label="성인 이미지 여부"
 | 
				
			||||||
 | 
					                  inset
 | 
				
			||||||
 | 
					                  :disabled="isEdit"
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              </v-col>
 | 
				
			||||||
 | 
					            </v-row>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <v-row>
 | 
				
			||||||
 | 
					              <v-col cols="12">
 | 
				
			||||||
 | 
					                <v-combobox
 | 
				
			||||||
 | 
					                  v-model="triggers"
 | 
				
			||||||
 | 
					                  label="트리거 단어 입력"
 | 
				
			||||||
 | 
					                  multiple
 | 
				
			||||||
 | 
					                  chips
 | 
				
			||||||
 | 
					                  small-chips
 | 
				
			||||||
 | 
					                  deletable-chips
 | 
				
			||||||
 | 
					                  outlined
 | 
				
			||||||
 | 
					                  dense
 | 
				
			||||||
 | 
					                  :rules="triggerRules"
 | 
				
			||||||
 | 
					                  @keydown.space.prevent="addTrigger"
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  <template v-slot:selection="{ attrs, item, select, selected }">
 | 
				
			||||||
 | 
					                    <v-chip
 | 
				
			||||||
 | 
					                      v-bind="attrs"
 | 
				
			||||||
 | 
					                      :input-value="selected"
 | 
				
			||||||
 | 
					                      close
 | 
				
			||||||
 | 
					                      @click="select"
 | 
				
			||||||
 | 
					                      @click:close="removeTrigger(item)"
 | 
				
			||||||
 | 
					                    >
 | 
				
			||||||
 | 
					                      {{ item }}
 | 
				
			||||||
 | 
					                    </v-chip>
 | 
				
			||||||
 | 
					                  </template>
 | 
				
			||||||
 | 
					                </v-combobox>
 | 
				
			||||||
 | 
					                <div class="caption grey--text text--darken-1">
 | 
				
			||||||
 | 
					                  트리거를 입력하고 엔터를 누르면 추가됩니다. (20자 이내, 최소 3개, 최대 10개)
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					              </v-col>
 | 
				
			||||||
 | 
					            </v-row>
 | 
				
			||||||
 | 
					          </v-card-text>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <v-card-actions>
 | 
				
			||||||
 | 
					            <v-spacer />
 | 
				
			||||||
 | 
					            <v-btn
 | 
				
			||||||
 | 
					              text
 | 
				
			||||||
 | 
					              color="blue darken-1"
 | 
				
			||||||
 | 
					              @click="goBack"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              취소
 | 
				
			||||||
 | 
					            </v-btn>
 | 
				
			||||||
 | 
					            <v-btn
 | 
				
			||||||
 | 
					              text
 | 
				
			||||||
 | 
					              color="blue darken-1"
 | 
				
			||||||
 | 
					              :disabled="!canSubmit || isSubmitting"
 | 
				
			||||||
 | 
					              :loading="isSubmitting"
 | 
				
			||||||
 | 
					              @click="save"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              저장
 | 
				
			||||||
 | 
					            </v-btn>
 | 
				
			||||||
 | 
					          </v-card-actions>
 | 
				
			||||||
 | 
					        </v-form>
 | 
				
			||||||
 | 
					      </v-card>
 | 
				
			||||||
 | 
					    </v-container>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
					import { createCharacterImage, updateCharacterImage, getCharacterImage } from '@/api/character'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default {
 | 
				
			||||||
 | 
					  name: 'CharacterImageForm',
 | 
				
			||||||
 | 
					  data() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      isEdit: !!this.$route.query.imageId,
 | 
				
			||||||
 | 
					      isSubmitting: false,
 | 
				
			||||||
 | 
					      isFormValid: false,
 | 
				
			||||||
 | 
					      characterId: Number(this.$route.query.characterId),
 | 
				
			||||||
 | 
					      imageId: this.$route.query.imageId ? Number(this.$route.query.imageId) : null,
 | 
				
			||||||
 | 
					      form: {
 | 
				
			||||||
 | 
					        image: null,
 | 
				
			||||||
 | 
					        imageUrl: '',
 | 
				
			||||||
 | 
					        soloPurchasePriceCan: null,
 | 
				
			||||||
 | 
					        messagePurchasePriceCan: null,
 | 
				
			||||||
 | 
					        adult: false
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      previewImage: null,
 | 
				
			||||||
 | 
					      triggers: [],
 | 
				
			||||||
 | 
					      triggerRules: [
 | 
				
			||||||
 | 
					        v => (v && v.length >= 3 && v.length <= 10) || '트리거는 최소 3개, 최대 10개까지 등록 가능합니다'
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      imageRules: [
 | 
				
			||||||
 | 
					        v => !!v || '이미지를 선택하세요'
 | 
				
			||||||
 | 
					      ]
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  computed: {
 | 
				
			||||||
 | 
					    canSubmit() {
 | 
				
			||||||
 | 
					      const triggersValid = this.triggers && this.triggers.length >= 3 && this.triggers.length <= 10
 | 
				
			||||||
 | 
					      if (this.isEdit) return triggersValid
 | 
				
			||||||
 | 
					      return !!this.form.image && triggersValid
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  watch: {
 | 
				
			||||||
 | 
					    'form.image'(newVal) {
 | 
				
			||||||
 | 
					      if (!this.isEdit) {
 | 
				
			||||||
 | 
					        if (newVal) this.createImagePreview(newVal)
 | 
				
			||||||
 | 
					        else this.previewImage = null
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  created() {
 | 
				
			||||||
 | 
					    if (!this.characterId) {
 | 
				
			||||||
 | 
					      this.notifyError('캐릭터 ID가 없습니다.')
 | 
				
			||||||
 | 
					      this.goBack();
 | 
				
			||||||
 | 
					      return
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (this.isEdit && this.imageId) {
 | 
				
			||||||
 | 
					      this.loadDetail()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  methods: {
 | 
				
			||||||
 | 
					    notifyError(m) { this.$dialog.notify.error(m) },
 | 
				
			||||||
 | 
					    notifySuccess(m) { this.$dialog.notify.success(m) },
 | 
				
			||||||
 | 
					    goBack() {
 | 
				
			||||||
 | 
					      this.$router.push({ path: '/character/images', query: { characterId: this.characterId, name: this.$route.query.name || '' } })
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    createImagePreview(file) {
 | 
				
			||||||
 | 
					      const reader = new FileReader()
 | 
				
			||||||
 | 
					      reader.onload = e => { this.previewImage = e.target.result }
 | 
				
			||||||
 | 
					      reader.readAsDataURL(file)
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    addTrigger(e) {
 | 
				
			||||||
 | 
					      const value = (e.target.value || '').trim()
 | 
				
			||||||
 | 
					      if (!value) return
 | 
				
			||||||
 | 
					      if (value.length > 20) {
 | 
				
			||||||
 | 
					        this.notifyError('트리거는 20자 이내여야 합니다.')
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (this.triggers.length >= 10) {
 | 
				
			||||||
 | 
					        this.notifyError('트리거는 최대 10개까지 등록 가능합니다.')
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (!this.triggers.includes(value)) this.triggers.push(value)
 | 
				
			||||||
 | 
					      e.target.value = ''
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    removeTrigger(item) {
 | 
				
			||||||
 | 
					      this.triggers = this.triggers.filter(t => t !== item)
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    async loadDetail() {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const resp = await getCharacterImage(this.imageId)
 | 
				
			||||||
 | 
					        if (resp && resp.status === 200 && resp.data && resp.data.success === true) {
 | 
				
			||||||
 | 
					          const d = resp.data.data
 | 
				
			||||||
 | 
					          // 수정 시 트리거만 노출하며 나머지는 비활성화
 | 
				
			||||||
 | 
					          this.form.imageUrl = d.imageUrl
 | 
				
			||||||
 | 
					          this.form.soloPurchasePriceCan = d.imagePriceCan
 | 
				
			||||||
 | 
					          this.form.messagePurchasePriceCan = d.messagePriceCan
 | 
				
			||||||
 | 
					          this.form.adult = d.isAdult
 | 
				
			||||||
 | 
					          this.triggers = d.triggers || []
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          this.notifyError('이미지 정보를 불러오지 못했습니다.')
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch (e) {
 | 
				
			||||||
 | 
					        console.error('이미지 상세 오류:', e)
 | 
				
			||||||
 | 
					        this.notifyError('이미지 정보를 불러오지 못했습니다.')
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    async save() {
 | 
				
			||||||
 | 
					      if (this.isSubmitting) return
 | 
				
			||||||
 | 
					      // 트리거 개수 검증: 최소 3개, 최대 10개
 | 
				
			||||||
 | 
					      if (!this.triggers || this.triggers.length < 3 || this.triggers.length > 10) {
 | 
				
			||||||
 | 
					        this.notifyError('트리거는 최소 3개, 최대 10개여야 합니다.')
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      this.isSubmitting = true
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        if (this.isEdit) {
 | 
				
			||||||
 | 
					          const resp = await updateCharacterImage({ imageId: this.imageId, triggers: this.triggers })
 | 
				
			||||||
 | 
					          if (resp && resp.status === 200 && resp.data && resp.data.success === true) {
 | 
				
			||||||
 | 
					            this.notifySuccess('수정되었습니다.')
 | 
				
			||||||
 | 
					            this.goBack()
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            this.notifyError('수정에 실패했습니다.')
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          const resp = await createCharacterImage({
 | 
				
			||||||
 | 
					            characterId: this.characterId,
 | 
				
			||||||
 | 
					            image: this.form.image,
 | 
				
			||||||
 | 
					            imagePriceCan: this.form.soloPurchasePriceCan,
 | 
				
			||||||
 | 
					            messagePriceCan: this.form.messagePurchasePriceCan,
 | 
				
			||||||
 | 
					            isAdult: this.form.adult,
 | 
				
			||||||
 | 
					            triggers: this.triggers
 | 
				
			||||||
 | 
					          })
 | 
				
			||||||
 | 
					          if (resp && resp.status === 200 && resp.data && resp.data.success === true) {
 | 
				
			||||||
 | 
					            this.notifySuccess('등록되었습니다.')
 | 
				
			||||||
 | 
					            this.goBack()
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            this.notifyError('등록에 실패했습니다.')
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch (e) {
 | 
				
			||||||
 | 
					        console.error('이미지 저장 오류:', e)
 | 
				
			||||||
 | 
					        this.notifyError('작업 중 오류가 발생했습니다.')
 | 
				
			||||||
 | 
					      } finally {
 | 
				
			||||||
 | 
					        this.isSubmitting = false
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style scoped>
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										325
									
								
								src/views/Chat/CharacterImageList.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										325
									
								
								src/views/Chat/CharacterImageList.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,325 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div>
 | 
				
			||||||
 | 
					    <v-toolbar dark>
 | 
				
			||||||
 | 
					      <v-btn
 | 
				
			||||||
 | 
					        icon
 | 
				
			||||||
 | 
					        @click="goBack"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <v-icon>mdi-arrow-left</v-icon>
 | 
				
			||||||
 | 
					      </v-btn>
 | 
				
			||||||
 | 
					      <v-spacer />
 | 
				
			||||||
 | 
					      <v-toolbar-title>캐릭터 이미지 관리</v-toolbar-title>
 | 
				
			||||||
 | 
					      <v-spacer />
 | 
				
			||||||
 | 
					    </v-toolbar>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <v-container>
 | 
				
			||||||
 | 
					      <v-row class="align-center mb-4">
 | 
				
			||||||
 | 
					        <v-col
 | 
				
			||||||
 | 
					          cols="12"
 | 
				
			||||||
 | 
					          md="6"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <div class="subtitle-1">
 | 
				
			||||||
 | 
					            캐릭터: {{ characterName || characterId }}
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </v-col>
 | 
				
			||||||
 | 
					        <v-col
 | 
				
			||||||
 | 
					          cols="12"
 | 
				
			||||||
 | 
					          md="6"
 | 
				
			||||||
 | 
					          class="text-right"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <v-btn
 | 
				
			||||||
 | 
					            color="primary"
 | 
				
			||||||
 | 
					            dark
 | 
				
			||||||
 | 
					            @click="goToAdd"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            이미지 추가
 | 
				
			||||||
 | 
					          </v-btn>
 | 
				
			||||||
 | 
					        </v-col>
 | 
				
			||||||
 | 
					      </v-row>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <!-- 로딩 -->
 | 
				
			||||||
 | 
					      <v-row v-if="isLoading && images.length === 0">
 | 
				
			||||||
 | 
					        <v-col class="text-center">
 | 
				
			||||||
 | 
					          <v-progress-circular
 | 
				
			||||||
 | 
					            indeterminate
 | 
				
			||||||
 | 
					            color="primary"
 | 
				
			||||||
 | 
					            size="48"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </v-col>
 | 
				
			||||||
 | 
					      </v-row>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <!-- 목록 -->
 | 
				
			||||||
 | 
					      <draggable
 | 
				
			||||||
 | 
					        v-if="images.length > 0"
 | 
				
			||||||
 | 
					        v-model="images"
 | 
				
			||||||
 | 
					        class="image-grid"
 | 
				
			||||||
 | 
					        :options="{ animation: 150 }"
 | 
				
			||||||
 | 
					        @end="onDragEnd"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <div
 | 
				
			||||||
 | 
					          v-for="img in images"
 | 
				
			||||||
 | 
					          :key="img.id"
 | 
				
			||||||
 | 
					          class="image-card"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <v-card>
 | 
				
			||||||
 | 
					            <div class="image-wrapper">
 | 
				
			||||||
 | 
					              <v-img
 | 
				
			||||||
 | 
					                :src="img.imageUrl"
 | 
				
			||||||
 | 
					                :aspect-ratio="0.8"
 | 
				
			||||||
 | 
					                contain
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					              <div
 | 
				
			||||||
 | 
					                v-if="img.isAdult"
 | 
				
			||||||
 | 
					                class="ribbon"
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                성인
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <v-card-text class="pt-2">
 | 
				
			||||||
 | 
					              <div class="price-row d-flex align-center">
 | 
				
			||||||
 | 
					                <div class="price-label">
 | 
				
			||||||
 | 
					                  단독 :
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                <div class="price-value">
 | 
				
			||||||
 | 
					                  {{ img.imagePriceCan }} 캔
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					              <div class="price-row d-flex align-center">
 | 
				
			||||||
 | 
					                <div class="price-label">
 | 
				
			||||||
 | 
					                  메시지 :
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                <div class="price-value">
 | 
				
			||||||
 | 
					                  {{ img.messagePriceCan }} 캔
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					              <div class="mt-2">
 | 
				
			||||||
 | 
					                <v-chip
 | 
				
			||||||
 | 
					                  v-for="(t, i) in (img.triggers || [])"
 | 
				
			||||||
 | 
					                  :key="i"
 | 
				
			||||||
 | 
					                  small
 | 
				
			||||||
 | 
					                  class="ma-1"
 | 
				
			||||||
 | 
					                  color="primary"
 | 
				
			||||||
 | 
					                  text-color="white"
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  {{ t }}
 | 
				
			||||||
 | 
					                </v-chip>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </v-card-text>
 | 
				
			||||||
 | 
					            <v-card-actions>
 | 
				
			||||||
 | 
					              <v-spacer />
 | 
				
			||||||
 | 
					              <v-btn
 | 
				
			||||||
 | 
					                small
 | 
				
			||||||
 | 
					                color="primary"
 | 
				
			||||||
 | 
					                @click="goToEdit(img)"
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                수정
 | 
				
			||||||
 | 
					              </v-btn>
 | 
				
			||||||
 | 
					              <v-btn
 | 
				
			||||||
 | 
					                small
 | 
				
			||||||
 | 
					                color="error"
 | 
				
			||||||
 | 
					                @click="confirmDelete(img)"
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                삭제
 | 
				
			||||||
 | 
					              </v-btn>
 | 
				
			||||||
 | 
					            </v-card-actions>
 | 
				
			||||||
 | 
					          </v-card>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </draggable>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <!-- 데이터 없음 -->
 | 
				
			||||||
 | 
					      <v-row v-if="!isLoading && images.length === 0">
 | 
				
			||||||
 | 
					        <v-col class="text-center grey--text">
 | 
				
			||||||
 | 
					          등록된 이미지가 없습니다.
 | 
				
			||||||
 | 
					        </v-col>
 | 
				
			||||||
 | 
					      </v-row>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <!-- 삭제 확인 다이얼로그 -->
 | 
				
			||||||
 | 
					      <v-dialog
 | 
				
			||||||
 | 
					        v-model="showDeleteDialog"
 | 
				
			||||||
 | 
					        max-width="400"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <v-card>
 | 
				
			||||||
 | 
					          <v-card-title class="headline">
 | 
				
			||||||
 | 
					            이미지 삭제
 | 
				
			||||||
 | 
					          </v-card-title>
 | 
				
			||||||
 | 
					          <v-card-text>삭제하시겠습니까?</v-card-text>
 | 
				
			||||||
 | 
					          <v-card-actions>
 | 
				
			||||||
 | 
					            <v-spacer />
 | 
				
			||||||
 | 
					            <v-btn
 | 
				
			||||||
 | 
					              text
 | 
				
			||||||
 | 
					              color="blue darken-1"
 | 
				
			||||||
 | 
					              @click="showDeleteDialog = false"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              취소
 | 
				
			||||||
 | 
					            </v-btn>
 | 
				
			||||||
 | 
					            <v-btn
 | 
				
			||||||
 | 
					              text
 | 
				
			||||||
 | 
					              color="red darken-1"
 | 
				
			||||||
 | 
					              :loading="isSubmitting"
 | 
				
			||||||
 | 
					              @click="deleteImage"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              삭제
 | 
				
			||||||
 | 
					            </v-btn>
 | 
				
			||||||
 | 
					          </v-card-actions>
 | 
				
			||||||
 | 
					        </v-card>
 | 
				
			||||||
 | 
					      </v-dialog>
 | 
				
			||||||
 | 
					    </v-container>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
					import { getCharacterImageList, deleteCharacterImage, updateCharacterImageOrder } from '@/api/character'
 | 
				
			||||||
 | 
					import draggable from 'vuedraggable'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default {
 | 
				
			||||||
 | 
					  name: 'CharacterImageList',
 | 
				
			||||||
 | 
					  components: { draggable },
 | 
				
			||||||
 | 
					  data() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      isLoading: false,
 | 
				
			||||||
 | 
					      isSubmitting: false,
 | 
				
			||||||
 | 
					      images: [],
 | 
				
			||||||
 | 
					      characterId: null,
 | 
				
			||||||
 | 
					      characterName: this.$route.query.name || '',
 | 
				
			||||||
 | 
					      showDeleteDialog: false,
 | 
				
			||||||
 | 
					      selectedImage: null
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  created() {
 | 
				
			||||||
 | 
					    this.characterId = Number(this.$route.query.characterId)
 | 
				
			||||||
 | 
					    if (!this.characterId) {
 | 
				
			||||||
 | 
					      this.notifyError('캐릭터 ID가 없습니다.');
 | 
				
			||||||
 | 
					      this.goBack()
 | 
				
			||||||
 | 
					      return
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    this.loadImages()
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  methods: {
 | 
				
			||||||
 | 
					    notifyError(message) { this.$dialog.notify.error(message) },
 | 
				
			||||||
 | 
					    notifySuccess(message) { this.$dialog.notify.success(message) },
 | 
				
			||||||
 | 
					    goBack() { this.$router.push('/character') },
 | 
				
			||||||
 | 
					    async loadImages() {
 | 
				
			||||||
 | 
					      this.isLoading = true
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const resp = await getCharacterImageList(this.characterId, 1, 20)
 | 
				
			||||||
 | 
					        if (resp && resp.status === 200 && resp.data && resp.data.success === true) {
 | 
				
			||||||
 | 
					          const data = resp.data.data
 | 
				
			||||||
 | 
					          this.images = (data.content || data || [])
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          this.notifyError('이미지 목록을 불러오지 못했습니다.')
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch (e) {
 | 
				
			||||||
 | 
					        console.error('이미지 목록 오류:', e)
 | 
				
			||||||
 | 
					        this.notifyError('이미지 목록 조회 중 오류가 발생했습니다.')
 | 
				
			||||||
 | 
					      } finally {
 | 
				
			||||||
 | 
					        this.isLoading = false
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    goToAdd() {
 | 
				
			||||||
 | 
					      this.$router.push({ path: '/character/images/form', query: { characterId: this.characterId, name: this.characterName } })
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    goToEdit(img) {
 | 
				
			||||||
 | 
					      this.$router.push({ path: '/character/images/form', query: { characterId: this.characterId, imageId: img.id, name: this.characterName } })
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    confirmDelete(img) {
 | 
				
			||||||
 | 
					      this.selectedImage = img
 | 
				
			||||||
 | 
					      this.showDeleteDialog = true
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    async deleteImage() {
 | 
				
			||||||
 | 
					      if (!this.selectedImage || this.isSubmitting) return
 | 
				
			||||||
 | 
					      this.isSubmitting = true
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const resp = await deleteCharacterImage(this.selectedImage.id)
 | 
				
			||||||
 | 
					        if (resp && resp.status === 200 && resp.data && resp.data.success === true) {
 | 
				
			||||||
 | 
					          this.notifySuccess('삭제되었습니다.')
 | 
				
			||||||
 | 
					          this.showDeleteDialog = false
 | 
				
			||||||
 | 
					          await this.loadImages()
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          this.notifyError('삭제에 실패했습니다.')
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch (e) {
 | 
				
			||||||
 | 
					        console.error('이미지 삭제 오류:', e)
 | 
				
			||||||
 | 
					        this.notifyError('삭제 중 오류가 발생했습니다.')
 | 
				
			||||||
 | 
					      } finally {
 | 
				
			||||||
 | 
					        this.isSubmitting = false
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    async onDragEnd() {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const ids = this.images.map(img => img.id)
 | 
				
			||||||
 | 
					        const resp = await updateCharacterImageOrder(this.characterId, ids)
 | 
				
			||||||
 | 
					        if (resp && resp.status === 200 && resp.data && resp.data.success === true) {
 | 
				
			||||||
 | 
					          this.notifySuccess('이미지 순서가 변경되었습니다.')
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          this.notifyError('이미지 순서 변경에 실패했습니다.')
 | 
				
			||||||
 | 
					          await this.loadImages()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch (e) {
 | 
				
			||||||
 | 
					        console.error('이미지 순서 변경 오류:', e)
 | 
				
			||||||
 | 
					        this.notifyError('이미지 순서 변경에 실패했습니다.')
 | 
				
			||||||
 | 
					        await this.loadImages()
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style scoped>
 | 
				
			||||||
 | 
					.image-grid {
 | 
				
			||||||
 | 
					  display: grid;
 | 
				
			||||||
 | 
					  grid-template-columns: repeat(5, 1fr);
 | 
				
			||||||
 | 
					  gap: 16px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.image-card {
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					@media (max-width: 1264px) {
 | 
				
			||||||
 | 
					  .image-grid { grid-template-columns: repeat(4, 1fr); }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					@media (max-width: 960px) {
 | 
				
			||||||
 | 
					  .image-grid { grid-template-columns: repeat(3, 1fr); }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					@media (max-width: 600px) {
 | 
				
			||||||
 | 
					  .image-grid { grid-template-columns: repeat(2, 1fr); }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Image wrapper for overlays */
 | 
				
			||||||
 | 
					.image-wrapper {
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Ribbon style for adult indicator */
 | 
				
			||||||
 | 
					.ribbon {
 | 
				
			||||||
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					  top: 0;
 | 
				
			||||||
 | 
					  right: 0;
 | 
				
			||||||
 | 
					  z-index: 2;
 | 
				
			||||||
 | 
					  background: #e53935; /* red darken-1 */
 | 
				
			||||||
 | 
					  color: #fff;
 | 
				
			||||||
 | 
					  padding: 6px 20px;
 | 
				
			||||||
 | 
					  font-weight: 500;
 | 
				
			||||||
 | 
					  font-size: 14px;
 | 
				
			||||||
 | 
					  text-transform: none;
 | 
				
			||||||
 | 
					  box-shadow: 0 2px 6px rgba(0,0,0,0.2);
 | 
				
			||||||
 | 
					  pointer-events: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Price rows styling */
 | 
				
			||||||
 | 
					.price-row {
 | 
				
			||||||
 | 
					  font-size: 16px;
 | 
				
			||||||
 | 
					  line-height: 1.6;
 | 
				
			||||||
 | 
					  margin-bottom: 4px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.price-label {
 | 
				
			||||||
 | 
					  width: 72px; /* 긴 쪽 기준으로 라벨 고정폭 */
 | 
				
			||||||
 | 
					  text-align: left;
 | 
				
			||||||
 | 
					  color: rgba(0,0,0,0.6);
 | 
				
			||||||
 | 
					  font-weight: 700;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.price-value {
 | 
				
			||||||
 | 
					  flex: 1;
 | 
				
			||||||
 | 
					  font-weight: 700;
 | 
				
			||||||
 | 
					  color: rgba(0,0,0,0.87);
 | 
				
			||||||
 | 
					  text-align: left;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										438
									
								
								src/views/Chat/CharacterList.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										438
									
								
								src/views/Chat/CharacterList.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,438 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div>
 | 
				
			||||||
 | 
					    <v-toolbar dark>
 | 
				
			||||||
 | 
					      <v-spacer />
 | 
				
			||||||
 | 
					      <v-toolbar-title>캐릭터 리스트</v-toolbar-title>
 | 
				
			||||||
 | 
					      <v-spacer />
 | 
				
			||||||
 | 
					    </v-toolbar>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <br>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <v-container>
 | 
				
			||||||
 | 
					      <v-row align="center">
 | 
				
			||||||
 | 
					        <v-col cols="4">
 | 
				
			||||||
 | 
					          <v-btn
 | 
				
			||||||
 | 
					            color="primary"
 | 
				
			||||||
 | 
					            dark
 | 
				
			||||||
 | 
					            @click="showAddDialog"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            캐릭터 추가
 | 
				
			||||||
 | 
					          </v-btn>
 | 
				
			||||||
 | 
					        </v-col>
 | 
				
			||||||
 | 
					        <v-col
 | 
				
			||||||
 | 
					          cols="8"
 | 
				
			||||||
 | 
					          class="d-flex justify-end align-center"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <v-text-field
 | 
				
			||||||
 | 
					            v-model="searchTerm"
 | 
				
			||||||
 | 
					            label="검색어"
 | 
				
			||||||
 | 
					            placeholder="캐릭터명, 태그, mbti 검색"
 | 
				
			||||||
 | 
					            outlined
 | 
				
			||||||
 | 
					            dense
 | 
				
			||||||
 | 
					            hide-details
 | 
				
			||||||
 | 
					            style="max-width: 320px;"
 | 
				
			||||||
 | 
					            class="mr-2"
 | 
				
			||||||
 | 
					            @keyup.enter="onSearch"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					          <v-btn
 | 
				
			||||||
 | 
					            :style="{ backgroundColor: '#3bb9f1', color: 'white' }"
 | 
				
			||||||
 | 
					            :disabled="is_loading"
 | 
				
			||||||
 | 
					            @click="onSearch"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            검색
 | 
				
			||||||
 | 
					          </v-btn>
 | 
				
			||||||
 | 
					        </v-col>
 | 
				
			||||||
 | 
					      </v-row>
 | 
				
			||||||
 | 
					      <v-row>
 | 
				
			||||||
 | 
					        <v-col>
 | 
				
			||||||
 | 
					          <v-simple-table class="elevation-10">
 | 
				
			||||||
 | 
					            <template>
 | 
				
			||||||
 | 
					              <thead>
 | 
				
			||||||
 | 
					                <tr>
 | 
				
			||||||
 | 
					                  <th class="text-center">
 | 
				
			||||||
 | 
					                    ID
 | 
				
			||||||
 | 
					                  </th>
 | 
				
			||||||
 | 
					                  <th class="text-center">
 | 
				
			||||||
 | 
					                    이미지
 | 
				
			||||||
 | 
					                  </th>
 | 
				
			||||||
 | 
					                  <th class="text-center">
 | 
				
			||||||
 | 
					                    캐릭터명
 | 
				
			||||||
 | 
					                  </th>
 | 
				
			||||||
 | 
					                  <th class="text-center">
 | 
				
			||||||
 | 
					                    성별
 | 
				
			||||||
 | 
					                  </th>
 | 
				
			||||||
 | 
					                  <th class="text-center">
 | 
				
			||||||
 | 
					                    나이
 | 
				
			||||||
 | 
					                  </th>
 | 
				
			||||||
 | 
					                  <th class="text-center">
 | 
				
			||||||
 | 
					                    캐릭터 설명
 | 
				
			||||||
 | 
					                  </th>
 | 
				
			||||||
 | 
					                  <th class="text-center">
 | 
				
			||||||
 | 
					                    MBTI
 | 
				
			||||||
 | 
					                  </th>
 | 
				
			||||||
 | 
					                  <th class="text-center">
 | 
				
			||||||
 | 
					                    말투
 | 
				
			||||||
 | 
					                  </th>
 | 
				
			||||||
 | 
					                  <th class="text-center">
 | 
				
			||||||
 | 
					                    대화 스타일
 | 
				
			||||||
 | 
					                  </th>
 | 
				
			||||||
 | 
					                  <th class="text-center">
 | 
				
			||||||
 | 
					                    태그
 | 
				
			||||||
 | 
					                  </th>
 | 
				
			||||||
 | 
					                  <th class="text-center">
 | 
				
			||||||
 | 
					                    등록일
 | 
				
			||||||
 | 
					                  </th>
 | 
				
			||||||
 | 
					                  <th class="text-center">
 | 
				
			||||||
 | 
					                    수정일
 | 
				
			||||||
 | 
					                  </th>
 | 
				
			||||||
 | 
					                  <th class="text-center">
 | 
				
			||||||
 | 
					                    관리
 | 
				
			||||||
 | 
					                  </th>
 | 
				
			||||||
 | 
					                </tr>
 | 
				
			||||||
 | 
					              </thead>
 | 
				
			||||||
 | 
					              <tbody>
 | 
				
			||||||
 | 
					                <tr
 | 
				
			||||||
 | 
					                  v-for="item in characters"
 | 
				
			||||||
 | 
					                  :key="item.id"
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  <td>{{ item.id }}</td>
 | 
				
			||||||
 | 
					                  <td align="center">
 | 
				
			||||||
 | 
					                    <v-img
 | 
				
			||||||
 | 
					                      max-width="100"
 | 
				
			||||||
 | 
					                      max-height="100"
 | 
				
			||||||
 | 
					                      :src="item.imageUrl"
 | 
				
			||||||
 | 
					                      class="rounded-circle"
 | 
				
			||||||
 | 
					                    />
 | 
				
			||||||
 | 
					                  </td>
 | 
				
			||||||
 | 
					                  <td>{{ item.name }}</td>
 | 
				
			||||||
 | 
					                  <td>{{ item.gender || '-' }}</td>
 | 
				
			||||||
 | 
					                  <td>{{ item.age || '-' }}</td>
 | 
				
			||||||
 | 
					                  <td>
 | 
				
			||||||
 | 
					                    <v-btn
 | 
				
			||||||
 | 
					                      small
 | 
				
			||||||
 | 
					                      color="info"
 | 
				
			||||||
 | 
					                      @click="showDetailDialog(item, 'description')"
 | 
				
			||||||
 | 
					                    >
 | 
				
			||||||
 | 
					                      보기
 | 
				
			||||||
 | 
					                    </v-btn>
 | 
				
			||||||
 | 
					                  </td>
 | 
				
			||||||
 | 
					                  <td>{{ item.mbti || '-' }}</td>
 | 
				
			||||||
 | 
					                  <td>
 | 
				
			||||||
 | 
					                    <v-btn
 | 
				
			||||||
 | 
					                      small
 | 
				
			||||||
 | 
					                      color="info"
 | 
				
			||||||
 | 
					                      @click="showDetailDialog(item, 'speechPattern')"
 | 
				
			||||||
 | 
					                    >
 | 
				
			||||||
 | 
					                      보기
 | 
				
			||||||
 | 
					                    </v-btn>
 | 
				
			||||||
 | 
					                  </td>
 | 
				
			||||||
 | 
					                  <td>
 | 
				
			||||||
 | 
					                    <v-btn
 | 
				
			||||||
 | 
					                      small
 | 
				
			||||||
 | 
					                      color="info"
 | 
				
			||||||
 | 
					                      @click="showDetailDialog(item, 'speechStyle')"
 | 
				
			||||||
 | 
					                    >
 | 
				
			||||||
 | 
					                      보기
 | 
				
			||||||
 | 
					                    </v-btn>
 | 
				
			||||||
 | 
					                  </td>
 | 
				
			||||||
 | 
					                  <td>
 | 
				
			||||||
 | 
					                    <div v-if="item.tags && item.tags.length > 0">
 | 
				
			||||||
 | 
					                      <v-chip
 | 
				
			||||||
 | 
					                        v-for="(tag, index) in item.tags"
 | 
				
			||||||
 | 
					                        :key="index"
 | 
				
			||||||
 | 
					                        small
 | 
				
			||||||
 | 
					                        class="ma-1"
 | 
				
			||||||
 | 
					                        color="primary"
 | 
				
			||||||
 | 
					                        text-color="white"
 | 
				
			||||||
 | 
					                      >
 | 
				
			||||||
 | 
					                        {{ tag }}
 | 
				
			||||||
 | 
					                      </v-chip>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                    <span v-else>-</span>
 | 
				
			||||||
 | 
					                  </td>
 | 
				
			||||||
 | 
					                  <td>{{ item.createdAt }}</td>
 | 
				
			||||||
 | 
					                  <td>{{ item.updatedAt || '-' }}</td>
 | 
				
			||||||
 | 
					                  <td>
 | 
				
			||||||
 | 
					                    <v-row>
 | 
				
			||||||
 | 
					                      <v-col>
 | 
				
			||||||
 | 
					                        <v-btn
 | 
				
			||||||
 | 
					                          small
 | 
				
			||||||
 | 
					                          color="primary"
 | 
				
			||||||
 | 
					                          :disabled="is_loading"
 | 
				
			||||||
 | 
					                          @click="showEditDialog(item)"
 | 
				
			||||||
 | 
					                        >
 | 
				
			||||||
 | 
					                          수정
 | 
				
			||||||
 | 
					                        </v-btn>
 | 
				
			||||||
 | 
					                      </v-col>
 | 
				
			||||||
 | 
					                      <v-col>
 | 
				
			||||||
 | 
					                        <v-btn
 | 
				
			||||||
 | 
					                          small
 | 
				
			||||||
 | 
					                          color="info"
 | 
				
			||||||
 | 
					                          :disabled="is_loading"
 | 
				
			||||||
 | 
					                          @click="goToImageList(item)"
 | 
				
			||||||
 | 
					                        >
 | 
				
			||||||
 | 
					                          이미지
 | 
				
			||||||
 | 
					                        </v-btn>
 | 
				
			||||||
 | 
					                      </v-col>
 | 
				
			||||||
 | 
					                      <v-col>
 | 
				
			||||||
 | 
					                        <v-btn
 | 
				
			||||||
 | 
					                          small
 | 
				
			||||||
 | 
					                          color="error"
 | 
				
			||||||
 | 
					                          :disabled="is_loading"
 | 
				
			||||||
 | 
					                          @click="deleteConfirm(item)"
 | 
				
			||||||
 | 
					                        >
 | 
				
			||||||
 | 
					                          삭제
 | 
				
			||||||
 | 
					                        </v-btn>
 | 
				
			||||||
 | 
					                      </v-col>
 | 
				
			||||||
 | 
					                    </v-row>
 | 
				
			||||||
 | 
					                  </td>
 | 
				
			||||||
 | 
					                </tr>
 | 
				
			||||||
 | 
					              </tbody>
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					          </v-simple-table>
 | 
				
			||||||
 | 
					        </v-col>
 | 
				
			||||||
 | 
					      </v-row>
 | 
				
			||||||
 | 
					      <v-row class="text-center">
 | 
				
			||||||
 | 
					        <v-col>
 | 
				
			||||||
 | 
					          <v-pagination
 | 
				
			||||||
 | 
					            v-model="page"
 | 
				
			||||||
 | 
					            :length="total_page"
 | 
				
			||||||
 | 
					            circle
 | 
				
			||||||
 | 
					            @input="next"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </v-col>
 | 
				
			||||||
 | 
					      </v-row>
 | 
				
			||||||
 | 
					    </v-container>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <!-- 삭제 확인 다이얼로그 -->
 | 
				
			||||||
 | 
					    <v-dialog
 | 
				
			||||||
 | 
					      v-model="show_delete_confirm_dialog"
 | 
				
			||||||
 | 
					      max-width="400px"
 | 
				
			||||||
 | 
					      persistent
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <v-card>
 | 
				
			||||||
 | 
					        <v-card-text />
 | 
				
			||||||
 | 
					        <v-card-text>
 | 
				
			||||||
 | 
					          "{{ selected_character.name }}"을(를) 삭제하시겠습니까?
 | 
				
			||||||
 | 
					        </v-card-text>
 | 
				
			||||||
 | 
					        <v-card-actions v-show="!is_loading">
 | 
				
			||||||
 | 
					          <v-spacer />
 | 
				
			||||||
 | 
					          <v-btn
 | 
				
			||||||
 | 
					            color="blue darken-1"
 | 
				
			||||||
 | 
					            text
 | 
				
			||||||
 | 
					            @click="closeDeleteDialog"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            취소
 | 
				
			||||||
 | 
					          </v-btn>
 | 
				
			||||||
 | 
					          <v-btn
 | 
				
			||||||
 | 
					            color="red darken-1"
 | 
				
			||||||
 | 
					            text
 | 
				
			||||||
 | 
					            @click="deleteCharacter"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            확인
 | 
				
			||||||
 | 
					          </v-btn>
 | 
				
			||||||
 | 
					        </v-card-actions>
 | 
				
			||||||
 | 
					      </v-card>
 | 
				
			||||||
 | 
					    </v-dialog>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <!-- 상세 내용 다이얼로그 -->
 | 
				
			||||||
 | 
					    <v-dialog
 | 
				
			||||||
 | 
					      v-model="show_detail_dialog"
 | 
				
			||||||
 | 
					      max-width="600px"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <v-card>
 | 
				
			||||||
 | 
					        <v-card-title>
 | 
				
			||||||
 | 
					          {{ detail_title }}
 | 
				
			||||||
 | 
					        </v-card-title>
 | 
				
			||||||
 | 
					        <v-divider />
 | 
				
			||||||
 | 
					        <v-card-text class="pt-4">
 | 
				
			||||||
 | 
					          <div style="white-space: pre-wrap;">
 | 
				
			||||||
 | 
					            {{ detail_content }}
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </v-card-text>
 | 
				
			||||||
 | 
					        <v-card-actions>
 | 
				
			||||||
 | 
					          <v-spacer />
 | 
				
			||||||
 | 
					          <v-btn
 | 
				
			||||||
 | 
					            color="primary"
 | 
				
			||||||
 | 
					            text
 | 
				
			||||||
 | 
					            @click="closeDetailDialog"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            닫기
 | 
				
			||||||
 | 
					          </v-btn>
 | 
				
			||||||
 | 
					        </v-card-actions>
 | 
				
			||||||
 | 
					      </v-card>
 | 
				
			||||||
 | 
					    </v-dialog>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
					import { getCharacterList, updateCharacter, searchCharacterList } from '@/api/character'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default {
 | 
				
			||||||
 | 
					  name: "CharacterList",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  data() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      is_loading: false,
 | 
				
			||||||
 | 
					      show_delete_confirm_dialog: false,
 | 
				
			||||||
 | 
					      show_detail_dialog: false,
 | 
				
			||||||
 | 
					      detail_type: '',
 | 
				
			||||||
 | 
					      detail_content: '',
 | 
				
			||||||
 | 
					      detail_title: '',
 | 
				
			||||||
 | 
					      page: 1,
 | 
				
			||||||
 | 
					      total_page: 0,
 | 
				
			||||||
 | 
					      characters: [],
 | 
				
			||||||
 | 
					      selected_character: {},
 | 
				
			||||||
 | 
					      searchTerm: ''
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async created() {
 | 
				
			||||||
 | 
					    await this.getCharacters()
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  methods: {
 | 
				
			||||||
 | 
					    notifyError(message) {
 | 
				
			||||||
 | 
					      this.$dialog.notify.error(message)
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    notifySuccess(message) {
 | 
				
			||||||
 | 
					      this.$dialog.notify.success(message)
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    showDetailDialog(item, type) {
 | 
				
			||||||
 | 
					      this.selected_character = item;
 | 
				
			||||||
 | 
					      this.detail_type = type;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // 타입에 따라 제목과 내용 설정
 | 
				
			||||||
 | 
					      switch(type) {
 | 
				
			||||||
 | 
					        case 'description':
 | 
				
			||||||
 | 
					          this.detail_title = '캐릭터 설명';
 | 
				
			||||||
 | 
					          this.detail_content = item.description || '내용이 없습니다.';
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        case 'speechPattern':
 | 
				
			||||||
 | 
					          this.detail_title = '말투';
 | 
				
			||||||
 | 
					          this.detail_content = item.speechPattern || '내용이 없습니다.';
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        case 'speechStyle':
 | 
				
			||||||
 | 
					          this.detail_title = '대화 스타일';
 | 
				
			||||||
 | 
					          this.detail_content = item.speechStyle || '내용이 없습니다.';
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        default:
 | 
				
			||||||
 | 
					          this.detail_title = '';
 | 
				
			||||||
 | 
					          this.detail_content = '';
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      this.show_detail_dialog = true;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    closeDetailDialog() {
 | 
				
			||||||
 | 
					      this.show_detail_dialog = false;
 | 
				
			||||||
 | 
					      this.detail_type = '';
 | 
				
			||||||
 | 
					      this.detail_content = '';
 | 
				
			||||||
 | 
					      this.detail_title = '';
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    showAddDialog() {
 | 
				
			||||||
 | 
					      // 페이지로 이동
 | 
				
			||||||
 | 
					      this.$router.push('/character/form');
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    goToImageList(item) {
 | 
				
			||||||
 | 
					      this.$router.push({
 | 
				
			||||||
 | 
					        path: '/character/images',
 | 
				
			||||||
 | 
					        query: { characterId: item.id, name: item.name }
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    showEditDialog(item) {
 | 
				
			||||||
 | 
					      // 페이지로 이동하면서 id 전달
 | 
				
			||||||
 | 
					      this.$router.push({
 | 
				
			||||||
 | 
					        path: '/character/form',
 | 
				
			||||||
 | 
					        query: { id: item.id }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    deleteConfirm(item) {
 | 
				
			||||||
 | 
					      this.selected_character = item
 | 
				
			||||||
 | 
					      this.show_delete_confirm_dialog = true
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    closeDeleteDialog() {
 | 
				
			||||||
 | 
					      this.show_delete_confirm_dialog = false
 | 
				
			||||||
 | 
					      this.selected_character = {}
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async deleteCharacter() {
 | 
				
			||||||
 | 
					      if (this.is_loading) return;
 | 
				
			||||||
 | 
					      this.is_loading = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        // 삭제 대신 isActive를 false로 설정하여 비활성화
 | 
				
			||||||
 | 
					        const updateData = {
 | 
				
			||||||
 | 
					          id: this.selected_character.id,
 | 
				
			||||||
 | 
					          isActive: false
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        await updateCharacter(updateData);
 | 
				
			||||||
 | 
					        this.closeDeleteDialog();
 | 
				
			||||||
 | 
					        this.notifySuccess('삭제되었습니다.');
 | 
				
			||||||
 | 
					        await this.getCharacters();
 | 
				
			||||||
 | 
					      } catch (e) {
 | 
				
			||||||
 | 
					        console.error('캐릭터 삭제 오류:', e);
 | 
				
			||||||
 | 
					        this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.');
 | 
				
			||||||
 | 
					      } finally {
 | 
				
			||||||
 | 
					        this.is_loading = false;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async next() {
 | 
				
			||||||
 | 
					      await this.getCharacters()
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    onSearch() {
 | 
				
			||||||
 | 
					      this.page = 1;
 | 
				
			||||||
 | 
					      this.getCharacters();
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async getCharacters() {
 | 
				
			||||||
 | 
					      this.is_loading = true
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const hasSearch = this.searchTerm && this.searchTerm.trim() !== '';
 | 
				
			||||||
 | 
					        const response = hasSearch
 | 
				
			||||||
 | 
					          ? await searchCharacterList(this.searchTerm.trim(), this.page, 20)
 | 
				
			||||||
 | 
					          : await getCharacterList(this.page);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (response && response.status === 200) {
 | 
				
			||||||
 | 
					          if (response.data.success === true) {
 | 
				
			||||||
 | 
					            const data = response.data.data;
 | 
				
			||||||
 | 
					            this.characters = data.content || [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const total_page = Math.ceil((data.totalCount || 0) / 20);
 | 
				
			||||||
 | 
					            this.total_page = total_page <= 0 ? 1 : total_page;
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            this.notifyError('응답 데이터가 없습니다.');
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch (e) {
 | 
				
			||||||
 | 
					        console.error('캐릭터 목록 조회 오류:', e);
 | 
				
			||||||
 | 
					        this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.');
 | 
				
			||||||
 | 
					      } finally {
 | 
				
			||||||
 | 
					        this.is_loading = false;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style scoped>
 | 
				
			||||||
 | 
					.v-data-table {
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										356
									
								
								src/views/Chat/OriginalDetail.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										356
									
								
								src/views/Chat/OriginalDetail.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,356 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div>
 | 
				
			||||||
 | 
					    <v-toolbar dark>
 | 
				
			||||||
 | 
					      <v-btn
 | 
				
			||||||
 | 
					        icon
 | 
				
			||||||
 | 
					        @click="goBack"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <v-icon>mdi-arrow-left</v-icon>
 | 
				
			||||||
 | 
					      </v-btn>
 | 
				
			||||||
 | 
					      <v-spacer />
 | 
				
			||||||
 | 
					      <v-toolbar-title>원작 상세</v-toolbar-title>
 | 
				
			||||||
 | 
					      <v-spacer />
 | 
				
			||||||
 | 
					      <v-btn
 | 
				
			||||||
 | 
					        color="primary"
 | 
				
			||||||
 | 
					        @click="openAssignDialog"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        캐릭터 연결
 | 
				
			||||||
 | 
					      </v-btn>
 | 
				
			||||||
 | 
					    </v-toolbar>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <v-container>
 | 
				
			||||||
 | 
					      <v-card
 | 
				
			||||||
 | 
					        v-if="detail"
 | 
				
			||||||
 | 
					        class="pa-4"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <v-row>
 | 
				
			||||||
 | 
					          <v-col
 | 
				
			||||||
 | 
					            cols="12"
 | 
				
			||||||
 | 
					            md="4"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <v-img
 | 
				
			||||||
 | 
					              :src="detail.imageUrl"
 | 
				
			||||||
 | 
					              contain
 | 
				
			||||||
 | 
					              height="240"
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          </v-col>
 | 
				
			||||||
 | 
					          <v-col
 | 
				
			||||||
 | 
					            cols="12"
 | 
				
			||||||
 | 
					            md="8"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <h2>{{ detail.title }}</h2>
 | 
				
			||||||
 | 
					            <div class="mt-2">
 | 
				
			||||||
 | 
					              콘텐츠 타입: {{ detail.contentType || '-' }}
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div>카테고리(장르): {{ detail.category || '-' }}</div>
 | 
				
			||||||
 | 
					            <div>19금 여부: {{ detail.isAdult ? '예' : '아니오' }}</div>
 | 
				
			||||||
 | 
					            <div>원천 원작: {{ detail.originalWork || '-' }}</div>
 | 
				
			||||||
 | 
					            <div class="mt-1">
 | 
				
			||||||
 | 
					              원천 원작 링크:
 | 
				
			||||||
 | 
					              <a
 | 
				
			||||||
 | 
					                v-if="detail.originalLink"
 | 
				
			||||||
 | 
					                :href="detail.originalLink"
 | 
				
			||||||
 | 
					                target="_blank"
 | 
				
			||||||
 | 
					                rel="noopener"
 | 
				
			||||||
 | 
					              >{{ detail.originalLink }}</a>
 | 
				
			||||||
 | 
					              <span v-else>-</span>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div>글/그림: {{ detail.writer || '-' }}</div>
 | 
				
			||||||
 | 
					            <div>제작사: {{ detail.studio || '-' }}</div>
 | 
				
			||||||
 | 
					            <div class="mt-1">
 | 
				
			||||||
 | 
					              원작 링크:
 | 
				
			||||||
 | 
					              <template v-if="detail.originalLinks && detail.originalLinks.length">
 | 
				
			||||||
 | 
					                <div>
 | 
				
			||||||
 | 
					                  <div
 | 
				
			||||||
 | 
					                    v-for="(link, idx) in detail.originalLinks"
 | 
				
			||||||
 | 
					                    :key="idx"
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                    <a
 | 
				
			||||||
 | 
					                      :href="link"
 | 
				
			||||||
 | 
					                      target="_blank"
 | 
				
			||||||
 | 
					                      rel="noopener"
 | 
				
			||||||
 | 
					                    >{{ link }}</a>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					              </template>
 | 
				
			||||||
 | 
					              <span v-else>-</span>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div class="mt-1">
 | 
				
			||||||
 | 
					              태그:
 | 
				
			||||||
 | 
					              <template v-if="detail.tags && detail.tags.length">
 | 
				
			||||||
 | 
					                <v-chip
 | 
				
			||||||
 | 
					                  v-for="(t, i) in detail.tags"
 | 
				
			||||||
 | 
					                  :key="i"
 | 
				
			||||||
 | 
					                  small
 | 
				
			||||||
 | 
					                  class="mr-1 mb-1"
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  {{ t }}
 | 
				
			||||||
 | 
					                </v-chip>
 | 
				
			||||||
 | 
					              </template>
 | 
				
			||||||
 | 
					              <span v-else>-</span>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div class="mt-2">
 | 
				
			||||||
 | 
					              작품 소개:
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div style="white-space:pre-wrap;">
 | 
				
			||||||
 | 
					              {{ detail.description || '-' }}
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </v-col>
 | 
				
			||||||
 | 
					        </v-row>
 | 
				
			||||||
 | 
					      </v-card>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <v-card class="pa-4 mt-6">
 | 
				
			||||||
 | 
					        <div class="d-flex align-center mb-4">
 | 
				
			||||||
 | 
					          <h3>연결된 캐릭터</h3>
 | 
				
			||||||
 | 
					          <v-spacer />
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <v-row>
 | 
				
			||||||
 | 
					          <v-col
 | 
				
			||||||
 | 
					            v-for="c in characters"
 | 
				
			||||||
 | 
					            :key="c.id"
 | 
				
			||||||
 | 
					            cols="12"
 | 
				
			||||||
 | 
					            sm="6"
 | 
				
			||||||
 | 
					            md="4"
 | 
				
			||||||
 | 
					            lg="3"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <v-card>
 | 
				
			||||||
 | 
					              <v-img
 | 
				
			||||||
 | 
					                :src="c.imagePath"
 | 
				
			||||||
 | 
					                height="180"
 | 
				
			||||||
 | 
					                contain
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					              <v-card-title class="text-no-wrap">
 | 
				
			||||||
 | 
					                {{ c.name }}
 | 
				
			||||||
 | 
					              </v-card-title>
 | 
				
			||||||
 | 
					              <v-card-actions>
 | 
				
			||||||
 | 
					                <v-spacer />
 | 
				
			||||||
 | 
					                <v-btn
 | 
				
			||||||
 | 
					                  small
 | 
				
			||||||
 | 
					                  color="error"
 | 
				
			||||||
 | 
					                  @click="unassign([c.id])"
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  해제
 | 
				
			||||||
 | 
					                </v-btn>
 | 
				
			||||||
 | 
					                <v-spacer />
 | 
				
			||||||
 | 
					              </v-card-actions>
 | 
				
			||||||
 | 
					            </v-card>
 | 
				
			||||||
 | 
					          </v-col>
 | 
				
			||||||
 | 
					        </v-row>
 | 
				
			||||||
 | 
					        <v-row v-if="isLoadingCharacters">
 | 
				
			||||||
 | 
					          <v-col class="text-center">
 | 
				
			||||||
 | 
					            <v-progress-circular
 | 
				
			||||||
 | 
					              indeterminate
 | 
				
			||||||
 | 
					              color="primary"
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          </v-col>
 | 
				
			||||||
 | 
					        </v-row>
 | 
				
			||||||
 | 
					      </v-card>
 | 
				
			||||||
 | 
					    </v-container>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <v-dialog
 | 
				
			||||||
 | 
					      v-model="assignDialog"
 | 
				
			||||||
 | 
					      max-width="800"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <v-card>
 | 
				
			||||||
 | 
					        <v-card-title>캐릭터 연결</v-card-title>
 | 
				
			||||||
 | 
					        <v-card-text>
 | 
				
			||||||
 | 
					          <v-text-field
 | 
				
			||||||
 | 
					            v-model="searchKeyword"
 | 
				
			||||||
 | 
					            label="캐릭터 검색"
 | 
				
			||||||
 | 
					            outlined
 | 
				
			||||||
 | 
					            dense
 | 
				
			||||||
 | 
					            @input="onSearchInput"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					          <v-data-table
 | 
				
			||||||
 | 
					            v-model="selectedToAssign"
 | 
				
			||||||
 | 
					            :headers="headers"
 | 
				
			||||||
 | 
					            :items="searchResults"
 | 
				
			||||||
 | 
					            :loading="searchLoading"
 | 
				
			||||||
 | 
					            item-key="id"
 | 
				
			||||||
 | 
					            show-select
 | 
				
			||||||
 | 
					            :items-per-page="5"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <template v-slot:item.imageUrl="{ item }">
 | 
				
			||||||
 | 
					              <v-img
 | 
				
			||||||
 | 
					                :src="item.imagePath"
 | 
				
			||||||
 | 
					                max-width="60"
 | 
				
			||||||
 | 
					                max-height="60"
 | 
				
			||||||
 | 
					                class="rounded-circle"
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					          </v-data-table>
 | 
				
			||||||
 | 
					        </v-card-text>
 | 
				
			||||||
 | 
					        <v-card-actions>
 | 
				
			||||||
 | 
					          <v-spacer />
 | 
				
			||||||
 | 
					          <v-btn
 | 
				
			||||||
 | 
					            text
 | 
				
			||||||
 | 
					            @click="assignDialog = false"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            취소
 | 
				
			||||||
 | 
					          </v-btn>
 | 
				
			||||||
 | 
					          <v-btn
 | 
				
			||||||
 | 
					            color="primary"
 | 
				
			||||||
 | 
					            :disabled="selectedToAssign.length===0"
 | 
				
			||||||
 | 
					            @click="assign"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            연결
 | 
				
			||||||
 | 
					          </v-btn>
 | 
				
			||||||
 | 
					        </v-card-actions>
 | 
				
			||||||
 | 
					      </v-card>
 | 
				
			||||||
 | 
					    </v-dialog>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
					import { getOriginal, getOriginalCharacters, assignCharactersToOriginal, unassignCharactersFromOriginal } from '@/api/original'
 | 
				
			||||||
 | 
					import { searchCharacters } from '@/api/character'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default {
 | 
				
			||||||
 | 
					  name: 'OriginalDetail',
 | 
				
			||||||
 | 
					  data() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      id: null,
 | 
				
			||||||
 | 
					      detail: null,
 | 
				
			||||||
 | 
					      characters: [],
 | 
				
			||||||
 | 
					      page: 1,
 | 
				
			||||||
 | 
					      hasMore: true,
 | 
				
			||||||
 | 
					      isLoadingCharacters: false,
 | 
				
			||||||
 | 
					      assignDialog: false,
 | 
				
			||||||
 | 
					      searchKeyword: '',
 | 
				
			||||||
 | 
					      searchLoading: false,
 | 
				
			||||||
 | 
					      searchResults: [],
 | 
				
			||||||
 | 
					      selectedToAssign: [],
 | 
				
			||||||
 | 
					      headers: [
 | 
				
			||||||
 | 
					        { text: '이미지', value: 'imageUrl', sortable: false },
 | 
				
			||||||
 | 
					        { text: '이름', value: 'name' },
 | 
				
			||||||
 | 
					        { text: 'ID', value: 'id' }
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      debounceTimer: null
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  created() {
 | 
				
			||||||
 | 
					    this.id = this.$route.query.id
 | 
				
			||||||
 | 
					    if (!this.id) {
 | 
				
			||||||
 | 
					      this.$dialog.notify.error('잘못된 접근입니다.');
 | 
				
			||||||
 | 
					      this.$router.push('/original-work');
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    this.loadDetail();
 | 
				
			||||||
 | 
					    this.loadCharacters();
 | 
				
			||||||
 | 
					    window.addEventListener('scroll', this.onScroll);
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  beforeDestroy() {
 | 
				
			||||||
 | 
					    window.removeEventListener('scroll', this.onScroll);
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  methods: {
 | 
				
			||||||
 | 
					    notifyError(message) { this.$dialog.notify.error(message) },
 | 
				
			||||||
 | 
					    notifySuccess(message) { this.$dialog.notify.success(message) },
 | 
				
			||||||
 | 
					    goBack() { this.$router.push('/original-work') },
 | 
				
			||||||
 | 
					    async loadDetail() {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const res = await getOriginal(this.id);
 | 
				
			||||||
 | 
					        if (res.status === 200 && res.data.success === true) {
 | 
				
			||||||
 | 
					          this.detail = res.data.data;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          this.notifyError('상세 조회 실패');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch (e) {
 | 
				
			||||||
 | 
					        this.notifyError('상세 조회 실패');
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    async loadCharacters() {
 | 
				
			||||||
 | 
					      if (this.isLoadingCharacters || !this.hasMore) return;
 | 
				
			||||||
 | 
					      this.isLoadingCharacters = true;
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const res = await getOriginalCharacters(this.id, this.page);
 | 
				
			||||||
 | 
					        if (res.status === 200 && res.data.success === true) {
 | 
				
			||||||
 | 
					          const content = res.data.data?.content || [];
 | 
				
			||||||
 | 
					          this.characters = this.characters.concat(content);
 | 
				
			||||||
 | 
					          this.hasMore = content.length > 0;
 | 
				
			||||||
 | 
					          this.page++;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          this.notifyError('캐릭터 목록 조회 실패');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch (e) {
 | 
				
			||||||
 | 
					        this.notifyError('캐릭터 목록 조회 실패');
 | 
				
			||||||
 | 
					      } finally {
 | 
				
			||||||
 | 
					        this.isLoadingCharacters = false;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    onScroll() {
 | 
				
			||||||
 | 
					      const scrollPosition = window.innerHeight + window.scrollY;
 | 
				
			||||||
 | 
					      const documentHeight = document.documentElement.offsetHeight;
 | 
				
			||||||
 | 
					      if (scrollPosition >= documentHeight - 200 && !this.isLoadingCharacters && this.hasMore) {
 | 
				
			||||||
 | 
					        this.loadCharacters();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    openAssignDialog() {
 | 
				
			||||||
 | 
					      this.assignDialog = true;
 | 
				
			||||||
 | 
					      this.searchKeyword = '';
 | 
				
			||||||
 | 
					      this.searchResults = [];
 | 
				
			||||||
 | 
					      this.selectedToAssign = [];
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    onSearchInput() {
 | 
				
			||||||
 | 
					      if (this.debounceTimer) clearTimeout(this.debounceTimer);
 | 
				
			||||||
 | 
					      this.debounceTimer = setTimeout(this.search, 300);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    async search() {
 | 
				
			||||||
 | 
					      if (!this.searchKeyword || !this.searchKeyword.trim()) {
 | 
				
			||||||
 | 
					        this.searchResults = [];
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      this.searchLoading = true;
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const res = await searchCharacters(this.searchKeyword.trim(), 1, 20);
 | 
				
			||||||
 | 
					        if (res.status === 200 && res.data.success === true) {
 | 
				
			||||||
 | 
					          this.searchResults = res.data.data?.content || [];
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          this.notifyError('검색 실패');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch (e) {
 | 
				
			||||||
 | 
					        this.notifyError('검색 실패');
 | 
				
			||||||
 | 
					      } finally {
 | 
				
			||||||
 | 
					        this.searchLoading = false;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    async assign() {
 | 
				
			||||||
 | 
					      if (this.selectedToAssign.length === 0) return;
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const ids = this.selectedToAssign.map(x => x.id);
 | 
				
			||||||
 | 
					        const res = await assignCharactersToOriginal(this.id, ids);
 | 
				
			||||||
 | 
					        if (res.status === 200 && res.data.success === true) {
 | 
				
			||||||
 | 
					          this.notifySuccess('연결되었습니다.');
 | 
				
			||||||
 | 
					          this.assignDialog = false;
 | 
				
			||||||
 | 
					          // 목록 초기화 후 재조회
 | 
				
			||||||
 | 
					          this.characters = [];
 | 
				
			||||||
 | 
					          this.page = 1;
 | 
				
			||||||
 | 
					          this.hasMore = true;
 | 
				
			||||||
 | 
					          this.loadCharacters();
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          this.notifyError('연결 실패');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch (e) {
 | 
				
			||||||
 | 
					        this.notifyError('연결 실패');
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    async unassign(ids) {
 | 
				
			||||||
 | 
					      if (!ids || ids.length === 0) return;
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const res = await unassignCharactersFromOriginal(this.id, ids);
 | 
				
			||||||
 | 
					        if (res.status === 200 && res.data.success === true) {
 | 
				
			||||||
 | 
					          this.notifySuccess('해제되었습니다.');
 | 
				
			||||||
 | 
					          this.characters = this.characters.filter(c => !ids.includes(c.id));
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          this.notifyError('해제 실패');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch (e) {
 | 
				
			||||||
 | 
					        this.notifyError('해제 실패');
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style scoped>
 | 
				
			||||||
 | 
					.text-no-wrap { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										505
									
								
								src/views/Chat/OriginalForm.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										505
									
								
								src/views/Chat/OriginalForm.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,505 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div>
 | 
				
			||||||
 | 
					    <v-toolbar dark>
 | 
				
			||||||
 | 
					      <v-btn
 | 
				
			||||||
 | 
					        icon
 | 
				
			||||||
 | 
					        @click="goBack"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <v-icon>mdi-arrow-left</v-icon>
 | 
				
			||||||
 | 
					      </v-btn>
 | 
				
			||||||
 | 
					      <v-spacer />
 | 
				
			||||||
 | 
					      <v-toolbar-title>{{ isEdit ? '원작 수정' : '원작 등록' }}</v-toolbar-title>
 | 
				
			||||||
 | 
					      <v-spacer />
 | 
				
			||||||
 | 
					    </v-toolbar>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <v-container>
 | 
				
			||||||
 | 
					      <v-card class="pa-4">
 | 
				
			||||||
 | 
					        <v-form
 | 
				
			||||||
 | 
					          ref="form"
 | 
				
			||||||
 | 
					          v-model="isFormValid"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <v-card-text>
 | 
				
			||||||
 | 
					            <v-row>
 | 
				
			||||||
 | 
					              <v-col
 | 
				
			||||||
 | 
					                cols="12"
 | 
				
			||||||
 | 
					                md="6"
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <v-file-input
 | 
				
			||||||
 | 
					                  v-model="form.image"
 | 
				
			||||||
 | 
					                  label="이미지"
 | 
				
			||||||
 | 
					                  accept="image/*"
 | 
				
			||||||
 | 
					                  prepend-icon="mdi-camera"
 | 
				
			||||||
 | 
					                  outlined
 | 
				
			||||||
 | 
					                  dense
 | 
				
			||||||
 | 
					                  :class="{ 'required-asterisk': !isEdit }"
 | 
				
			||||||
 | 
					                  :rules="imageRules"
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              </v-col>
 | 
				
			||||||
 | 
					              <v-col
 | 
				
			||||||
 | 
					                v-if="previewImage || form.imageUrl"
 | 
				
			||||||
 | 
					                cols="12"
 | 
				
			||||||
 | 
					                md="6"
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <div class="text-center">
 | 
				
			||||||
 | 
					                  <v-avatar size="150">
 | 
				
			||||||
 | 
					                    <v-img
 | 
				
			||||||
 | 
					                      :src="previewImage || form.imageUrl"
 | 
				
			||||||
 | 
					                      contain
 | 
				
			||||||
 | 
					                    />
 | 
				
			||||||
 | 
					                  </v-avatar>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					              </v-col>
 | 
				
			||||||
 | 
					            </v-row>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <v-row>
 | 
				
			||||||
 | 
					              <v-col cols="12">
 | 
				
			||||||
 | 
					                <v-text-field
 | 
				
			||||||
 | 
					                  v-model="form.title"
 | 
				
			||||||
 | 
					                  label="제목"
 | 
				
			||||||
 | 
					                  outlined
 | 
				
			||||||
 | 
					                  dense
 | 
				
			||||||
 | 
					                  :rules="[v=>!!v||'제목은 필수입니다']"
 | 
				
			||||||
 | 
					                  class="required-asterisk"
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              </v-col>
 | 
				
			||||||
 | 
					            </v-row>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <v-row>
 | 
				
			||||||
 | 
					              <v-col
 | 
				
			||||||
 | 
					                cols="12"
 | 
				
			||||||
 | 
					                md="6"
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <v-text-field
 | 
				
			||||||
 | 
					                  v-model="form.contentType"
 | 
				
			||||||
 | 
					                  label="콘텐츠 타입"
 | 
				
			||||||
 | 
					                  outlined
 | 
				
			||||||
 | 
					                  dense
 | 
				
			||||||
 | 
					                  :rules="contentTypeRules"
 | 
				
			||||||
 | 
					                  :class="{ 'required-asterisk': !isEdit }"
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              </v-col>
 | 
				
			||||||
 | 
					              <v-col
 | 
				
			||||||
 | 
					                cols="12"
 | 
				
			||||||
 | 
					                md="6"
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <v-text-field
 | 
				
			||||||
 | 
					                  v-model="form.category"
 | 
				
			||||||
 | 
					                  label="카테고리(장르)"
 | 
				
			||||||
 | 
					                  outlined
 | 
				
			||||||
 | 
					                  dense
 | 
				
			||||||
 | 
					                  :rules="categoryRules"
 | 
				
			||||||
 | 
					                  :class="{ 'required-asterisk': !isEdit }"
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              </v-col>
 | 
				
			||||||
 | 
					            </v-row>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <!-- 추가 메타 정보 (요구 순서: 글/그림, 제작사, 원천원작, 원천 원작 링크) -->
 | 
				
			||||||
 | 
					            <v-row>
 | 
				
			||||||
 | 
					              <v-col
 | 
				
			||||||
 | 
					                cols="12"
 | 
				
			||||||
 | 
					                md="6"
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <v-text-field
 | 
				
			||||||
 | 
					                  v-model="form.writer"
 | 
				
			||||||
 | 
					                  label="글/그림"
 | 
				
			||||||
 | 
					                  outlined
 | 
				
			||||||
 | 
					                  dense
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              </v-col>
 | 
				
			||||||
 | 
					              <v-col
 | 
				
			||||||
 | 
					                cols="12"
 | 
				
			||||||
 | 
					                md="6"
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <v-text-field
 | 
				
			||||||
 | 
					                  v-model="form.studio"
 | 
				
			||||||
 | 
					                  label="제작사"
 | 
				
			||||||
 | 
					                  outlined
 | 
				
			||||||
 | 
					                  dense
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              </v-col>
 | 
				
			||||||
 | 
					            </v-row>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <v-row>
 | 
				
			||||||
 | 
					              <v-col
 | 
				
			||||||
 | 
					                cols="12"
 | 
				
			||||||
 | 
					                md="6"
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <v-text-field
 | 
				
			||||||
 | 
					                  v-model="form.originalWork"
 | 
				
			||||||
 | 
					                  label="원천 원작"
 | 
				
			||||||
 | 
					                  outlined
 | 
				
			||||||
 | 
					                  dense
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              </v-col>
 | 
				
			||||||
 | 
					              <v-col
 | 
				
			||||||
 | 
					                cols="12"
 | 
				
			||||||
 | 
					                md="6"
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <v-text-field
 | 
				
			||||||
 | 
					                  v-model="form.originalLink"
 | 
				
			||||||
 | 
					                  label="원천 원작 링크"
 | 
				
			||||||
 | 
					                  outlined
 | 
				
			||||||
 | 
					                  dense
 | 
				
			||||||
 | 
					                  :rules="originalLinkRules"
 | 
				
			||||||
 | 
					                  :class="{ 'required-asterisk': !isEdit }"
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              </v-col>
 | 
				
			||||||
 | 
					            </v-row>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <v-row>
 | 
				
			||||||
 | 
					              <v-col
 | 
				
			||||||
 | 
					                cols="12"
 | 
				
			||||||
 | 
					                md="12"
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <v-switch
 | 
				
			||||||
 | 
					                  v-model="form.isAdult"
 | 
				
			||||||
 | 
					                  label="19금 여부"
 | 
				
			||||||
 | 
					                  inset
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              </v-col>
 | 
				
			||||||
 | 
					            </v-row>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <!-- 원작 링크(여러 개) 추가 -->
 | 
				
			||||||
 | 
					            <v-row>
 | 
				
			||||||
 | 
					              <v-col cols="12">
 | 
				
			||||||
 | 
					                <v-divider class="my-4" />
 | 
				
			||||||
 | 
					                <h3 class="mb-2">
 | 
				
			||||||
 | 
					                  원작 링크
 | 
				
			||||||
 | 
					                </h3>
 | 
				
			||||||
 | 
					                <v-row>
 | 
				
			||||||
 | 
					                  <v-col cols="11">
 | 
				
			||||||
 | 
					                    <v-text-field
 | 
				
			||||||
 | 
					                      v-model="newOriginalLink"
 | 
				
			||||||
 | 
					                      label="원작 링크 추가"
 | 
				
			||||||
 | 
					                      outlined
 | 
				
			||||||
 | 
					                      dense
 | 
				
			||||||
 | 
					                      @keyup.enter="addOriginalLink"
 | 
				
			||||||
 | 
					                    />
 | 
				
			||||||
 | 
					                  </v-col>
 | 
				
			||||||
 | 
					                  <v-col cols="1">
 | 
				
			||||||
 | 
					                    <v-btn
 | 
				
			||||||
 | 
					                      color="primary"
 | 
				
			||||||
 | 
					                      class="mt-1"
 | 
				
			||||||
 | 
					                      block
 | 
				
			||||||
 | 
					                      :disabled="!newOriginalLink || !newOriginalLink.trim()"
 | 
				
			||||||
 | 
					                      @click="addOriginalLink"
 | 
				
			||||||
 | 
					                    >
 | 
				
			||||||
 | 
					                      추가
 | 
				
			||||||
 | 
					                    </v-btn>
 | 
				
			||||||
 | 
					                  </v-col>
 | 
				
			||||||
 | 
					                </v-row>
 | 
				
			||||||
 | 
					                <v-card
 | 
				
			||||||
 | 
					                  outlined
 | 
				
			||||||
 | 
					                  class="mt-2"
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  <v-list v-if="form.originalLinks && form.originalLinks.length > 0">
 | 
				
			||||||
 | 
					                    <v-list-item
 | 
				
			||||||
 | 
					                      v-for="(link, idx) in form.originalLinks"
 | 
				
			||||||
 | 
					                      :key="idx"
 | 
				
			||||||
 | 
					                    >
 | 
				
			||||||
 | 
					                      <v-list-item-content>
 | 
				
			||||||
 | 
					                        <v-list-item-title class="text-truncate">
 | 
				
			||||||
 | 
					                          {{ link }}
 | 
				
			||||||
 | 
					                        </v-list-item-title>
 | 
				
			||||||
 | 
					                      </v-list-item-content>
 | 
				
			||||||
 | 
					                      <v-list-item-action>
 | 
				
			||||||
 | 
					                        <v-btn
 | 
				
			||||||
 | 
					                          small
 | 
				
			||||||
 | 
					                          color="error"
 | 
				
			||||||
 | 
					                          @click="removeOriginalLink(idx)"
 | 
				
			||||||
 | 
					                        >
 | 
				
			||||||
 | 
					                          삭제
 | 
				
			||||||
 | 
					                        </v-btn>
 | 
				
			||||||
 | 
					                      </v-list-item-action>
 | 
				
			||||||
 | 
					                    </v-list-item>
 | 
				
			||||||
 | 
					                  </v-list>
 | 
				
			||||||
 | 
					                  <v-card-text
 | 
				
			||||||
 | 
					                    v-else
 | 
				
			||||||
 | 
					                    class="grey--text"
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                    추가된 원작 링크가 없습니다.
 | 
				
			||||||
 | 
					                  </v-card-text>
 | 
				
			||||||
 | 
					                </v-card>
 | 
				
			||||||
 | 
					              </v-col>
 | 
				
			||||||
 | 
					            </v-row>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <!-- 태그 -->
 | 
				
			||||||
 | 
					            <v-row>
 | 
				
			||||||
 | 
					              <v-col cols="12">
 | 
				
			||||||
 | 
					                <v-divider class="my-4" />
 | 
				
			||||||
 | 
					                <h3 class="mb-2">
 | 
				
			||||||
 | 
					                  태그
 | 
				
			||||||
 | 
					                </h3>
 | 
				
			||||||
 | 
					                <v-combobox
 | 
				
			||||||
 | 
					                  v-model="form.tags"
 | 
				
			||||||
 | 
					                  multiple
 | 
				
			||||||
 | 
					                  chips
 | 
				
			||||||
 | 
					                  small-chips
 | 
				
			||||||
 | 
					                  deletable-chips
 | 
				
			||||||
 | 
					                  outlined
 | 
				
			||||||
 | 
					                  dense
 | 
				
			||||||
 | 
					                  label="태그를 입력 후 엔터로 추가"
 | 
				
			||||||
 | 
					                  @keydown.space.prevent="onTagSpace"
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  <template v-slot:selection="{ attrs, item, select, selected }">
 | 
				
			||||||
 | 
					                    <v-chip
 | 
				
			||||||
 | 
					                      v-bind="attrs"
 | 
				
			||||||
 | 
					                      :input-value="selected"
 | 
				
			||||||
 | 
					                      close
 | 
				
			||||||
 | 
					                      @click="select"
 | 
				
			||||||
 | 
					                      @click:close="removeTag(item)"
 | 
				
			||||||
 | 
					                    >
 | 
				
			||||||
 | 
					                      {{ item }}
 | 
				
			||||||
 | 
					                    </v-chip>
 | 
				
			||||||
 | 
					                  </template>
 | 
				
			||||||
 | 
					                </v-combobox>
 | 
				
			||||||
 | 
					              </v-col>
 | 
				
			||||||
 | 
					            </v-row>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <v-row>
 | 
				
			||||||
 | 
					              <v-col cols="12">
 | 
				
			||||||
 | 
					                <v-textarea
 | 
				
			||||||
 | 
					                  v-model="form.description"
 | 
				
			||||||
 | 
					                  label="작품 소개"
 | 
				
			||||||
 | 
					                  outlined
 | 
				
			||||||
 | 
					                  rows="4"
 | 
				
			||||||
 | 
					                  :rules="descriptionRules"
 | 
				
			||||||
 | 
					                  :class="{ 'required-asterisk': !isEdit }"
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              </v-col>
 | 
				
			||||||
 | 
					            </v-row>
 | 
				
			||||||
 | 
					          </v-card-text>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <v-card-actions>
 | 
				
			||||||
 | 
					            <v-spacer />
 | 
				
			||||||
 | 
					            <v-btn
 | 
				
			||||||
 | 
					              color="primary"
 | 
				
			||||||
 | 
					              :disabled="!canSubmit"
 | 
				
			||||||
 | 
					              @click="onSubmit"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              {{ isEdit ? '수정' : '등록' }}
 | 
				
			||||||
 | 
					            </v-btn>
 | 
				
			||||||
 | 
					          </v-card-actions>
 | 
				
			||||||
 | 
					        </v-form>
 | 
				
			||||||
 | 
					      </v-card>
 | 
				
			||||||
 | 
					    </v-container>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
					import { createOriginal, updateOriginal, getOriginal } from '@/api/original'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default {
 | 
				
			||||||
 | 
					  name: 'OriginalForm',
 | 
				
			||||||
 | 
					  data() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      isEdit: false,
 | 
				
			||||||
 | 
					      isFormValid: false,
 | 
				
			||||||
 | 
					      previewImage: null,
 | 
				
			||||||
 | 
					      newOriginalLink: '',
 | 
				
			||||||
 | 
					      form: {
 | 
				
			||||||
 | 
					        id: null,
 | 
				
			||||||
 | 
					        image: null,
 | 
				
			||||||
 | 
					        imageUrl: null,
 | 
				
			||||||
 | 
					        title: '',
 | 
				
			||||||
 | 
					        contentType: '',
 | 
				
			||||||
 | 
					        category: '',
 | 
				
			||||||
 | 
					        isAdult: false,
 | 
				
			||||||
 | 
					        description: '',
 | 
				
			||||||
 | 
					        originalLink: '', // 원천 원작 링크(파라미터명 유지)
 | 
				
			||||||
 | 
					        originalWork: '',
 | 
				
			||||||
 | 
					        writer: '',
 | 
				
			||||||
 | 
					        studio: '',
 | 
				
			||||||
 | 
					        originalLinks: [], // 추가 원작 링크들
 | 
				
			||||||
 | 
					        tags: []
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      originalInitial: null,
 | 
				
			||||||
 | 
					      imageRules: [v => (this.isEdit ? true : (!!v || '이미지를 선택하세요'))],
 | 
				
			||||||
 | 
					      contentTypeRules: [v => (this.isEdit ? true : (!!(v && v.toString().trim()) || '콘텐츠 타입은 필수입니다'))],
 | 
				
			||||||
 | 
					      categoryRules: [v => (this.isEdit ? true : (!!(v && v.toString().trim()) || '카테고리는 필수입니다'))],
 | 
				
			||||||
 | 
					      originalLinkRules: [v => (this.isEdit ? true : (!!(v && v.toString().trim()) || '원천 원작 링크는 필수입니다'))],
 | 
				
			||||||
 | 
					      descriptionRules: [v => (this.isEdit ? true : (!!(v && v.toString().trim()) || '작품 소개는 필수입니다'))]
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  computed: {
 | 
				
			||||||
 | 
					    imageChanged() {
 | 
				
			||||||
 | 
					      return !!this.form.image;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    hasNonImageChanges() {
 | 
				
			||||||
 | 
					      if (!this.isEdit || !this.originalInitial) return false;
 | 
				
			||||||
 | 
					      const fields = ['title', 'contentType', 'category', 'isAdult', 'description', 'originalLink', 'originalWork', 'writer', 'studio'];
 | 
				
			||||||
 | 
					      const basicChanged = fields.some(f => this.form[f] !== this.originalInitial[f]);
 | 
				
			||||||
 | 
					      const arraysChanged = !this.arraysEqual(this.form.originalLinks, this.originalInitial.originalLinks)
 | 
				
			||||||
 | 
					        || !this.arraysEqual(this.form.tags, this.originalInitial.tags);
 | 
				
			||||||
 | 
					      return basicChanged || arraysChanged;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    hasEditChanges() {
 | 
				
			||||||
 | 
					      return this.imageChanged || this.hasNonImageChanges;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    canSubmit() {
 | 
				
			||||||
 | 
					      if (this.isEdit) return this.hasEditChanges && !!(this.form.title && this.form.title.toString().trim());
 | 
				
			||||||
 | 
					      const required = [this.form.image, this.form.title, this.form.contentType, this.form.category, this.form.originalLink, this.form.description];
 | 
				
			||||||
 | 
					      return required.every(v => !!(v && (v.toString ? v.toString().trim() : v)));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  watch: {
 | 
				
			||||||
 | 
					    'form.image': {
 | 
				
			||||||
 | 
					      handler(newImage) {
 | 
				
			||||||
 | 
					        if (newImage) {
 | 
				
			||||||
 | 
					          const reader = new FileReader();
 | 
				
			||||||
 | 
					          reader.onload = (e) => { this.previewImage = e.target.result }
 | 
				
			||||||
 | 
					          reader.readAsDataURL(newImage)
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          this.previewImage = null
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  created() {
 | 
				
			||||||
 | 
					    if (this.$route.query.id) {
 | 
				
			||||||
 | 
					      this.isEdit = true;
 | 
				
			||||||
 | 
					      this.load(this.$route.query.id);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  methods: {
 | 
				
			||||||
 | 
					    notifyError(message) { this.$dialog.notify.error(message) },
 | 
				
			||||||
 | 
					    notifySuccess(message) { this.$dialog.notify.success(message) },
 | 
				
			||||||
 | 
					    goBack() { this.$router.push('/original-work') },
 | 
				
			||||||
 | 
					    arraysEqual(a, b) {
 | 
				
			||||||
 | 
					      const arrA = Array.isArray(a) ? a : [];
 | 
				
			||||||
 | 
					      const arrB = Array.isArray(b) ? b : [];
 | 
				
			||||||
 | 
					      if (arrA.length !== arrB.length) return false;
 | 
				
			||||||
 | 
					      for (let i = 0; i < arrA.length; i++) {
 | 
				
			||||||
 | 
					        if (arrA[i] !== arrB[i]) return false;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return true;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    addOriginalLink() {
 | 
				
			||||||
 | 
					      if (!this.newOriginalLink || !this.newOriginalLink.trim()) return;
 | 
				
			||||||
 | 
					      const val = this.newOriginalLink.trim();
 | 
				
			||||||
 | 
					      if (!this.form.originalLinks) this.form.originalLinks = [];
 | 
				
			||||||
 | 
					      if (!this.form.originalLinks.includes(val)) {
 | 
				
			||||||
 | 
					        this.form.originalLinks.push(val);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      this.newOriginalLink = '';
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    removeOriginalLink(index) {
 | 
				
			||||||
 | 
					      if (!this.form.originalLinks) return;
 | 
				
			||||||
 | 
					      this.form.originalLinks.splice(index, 1);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    onTagSpace() {
 | 
				
			||||||
 | 
					      // CharacterForm의 태그 방식과 유사: 마지막 항목을 공백 기준으로 확정
 | 
				
			||||||
 | 
					      if (!Array.isArray(this.form.tags)) this.form.tags = [];
 | 
				
			||||||
 | 
					      const last = this.form.tags[this.form.tags.length - 1];
 | 
				
			||||||
 | 
					      if (typeof last === 'string' && last.trim()) {
 | 
				
			||||||
 | 
					        let processed = last.trim().replace(/\s+/g, '');
 | 
				
			||||||
 | 
					        if (processed.length > 50) processed = processed.substring(0, 50);
 | 
				
			||||||
 | 
					        this.form.tags.splice(this.form.tags.length - 1, 1, processed);
 | 
				
			||||||
 | 
					        this.$nextTick(() => this.form.tags.push(''));
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    removeTag(item) {
 | 
				
			||||||
 | 
					      if (!Array.isArray(this.form.tags)) return;
 | 
				
			||||||
 | 
					      const idx = this.form.tags.indexOf(item);
 | 
				
			||||||
 | 
					      if (idx >= 0) this.form.tags.splice(idx, 1);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    async load(id) {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const res = await getOriginal(id);
 | 
				
			||||||
 | 
					        if (res.status === 200 && res.data.success === true) {
 | 
				
			||||||
 | 
					          const d = res.data.data;
 | 
				
			||||||
 | 
					          this.form = {
 | 
				
			||||||
 | 
					            id: d.id,
 | 
				
			||||||
 | 
					            image: null,
 | 
				
			||||||
 | 
					            imageUrl: d.imageUrl,
 | 
				
			||||||
 | 
					            title: d.title || '',
 | 
				
			||||||
 | 
					            contentType: d.contentType || '',
 | 
				
			||||||
 | 
					            category: d.category || '',
 | 
				
			||||||
 | 
					            isAdult: !!d.isAdult,
 | 
				
			||||||
 | 
					            description: d.description || '',
 | 
				
			||||||
 | 
					            originalLink: d.originalLink || '',
 | 
				
			||||||
 | 
					            originalWork: d.originalWork || '',
 | 
				
			||||||
 | 
					            writer: d.writer || '',
 | 
				
			||||||
 | 
					            studio: d.studio || '',
 | 
				
			||||||
 | 
					            originalLinks: Array.isArray(d.originalLinks) ? d.originalLinks.slice() : [],
 | 
				
			||||||
 | 
					            tags: Array.isArray(d.tags) ? d.tags.slice() : []
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          this.originalInitial = {
 | 
				
			||||||
 | 
					            id: d.id,
 | 
				
			||||||
 | 
					            imageUrl: d.imageUrl,
 | 
				
			||||||
 | 
					            title: d.title || '',
 | 
				
			||||||
 | 
					            contentType: d.contentType || '',
 | 
				
			||||||
 | 
					            category: d.category || '',
 | 
				
			||||||
 | 
					            isAdult: !!d.isAdult,
 | 
				
			||||||
 | 
					            description: d.description || '',
 | 
				
			||||||
 | 
					            originalLink: d.originalLink || '',
 | 
				
			||||||
 | 
					            originalWork: d.originalWork || '',
 | 
				
			||||||
 | 
					            writer: d.writer || '',
 | 
				
			||||||
 | 
					            studio: d.studio || '',
 | 
				
			||||||
 | 
					            originalLinks: Array.isArray(d.originalLinks) ? d.originalLinks.slice() : [],
 | 
				
			||||||
 | 
					            tags: Array.isArray(d.tags) ? d.tags.slice() : []
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          this.notifyError('상세 조회 실패');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch (e) {
 | 
				
			||||||
 | 
					        this.notifyError('상세 조회 실패');
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    async onSubmit() {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const isValid = this.$refs.form ? this.$refs.form.validate() : true;
 | 
				
			||||||
 | 
					        if (!isValid) {
 | 
				
			||||||
 | 
					          this.notifyError(this.isEdit ? '입력을 확인해주세요.' : '필수 항목을 모두 입력해주세요.');
 | 
				
			||||||
 | 
					          return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (this.isEdit) {
 | 
				
			||||||
 | 
					          const fields = ['title', 'contentType', 'category', 'isAdult', 'description', 'originalLink', 'originalWork', 'writer', 'studio'];
 | 
				
			||||||
 | 
					          const patch = { id: this.form.id };
 | 
				
			||||||
 | 
					          if (this.originalInitial) {
 | 
				
			||||||
 | 
					            fields.forEach(f => {
 | 
				
			||||||
 | 
					              if (this.form[f] !== this.originalInitial[f]) {
 | 
				
			||||||
 | 
					                patch[f] = this.form[f];
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            if (!this.arraysEqual(this.form.originalLinks, this.originalInitial.originalLinks)) {
 | 
				
			||||||
 | 
					              patch.originalLinks = this.form.originalLinks;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if (!this.arraysEqual(this.form.tags, this.originalInitial.tags)) {
 | 
				
			||||||
 | 
					              patch.tags = this.form.tags;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          const image = this.form.image || null;
 | 
				
			||||||
 | 
					          if (Object.keys(patch).length === 1 && !image) {
 | 
				
			||||||
 | 
					            this.notifyError('변경된 내용이 없습니다.');
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          const res = await updateOriginal(patch, image);
 | 
				
			||||||
 | 
					          if (res.status === 200 && res.data.success === true) {
 | 
				
			||||||
 | 
					            this.notifySuccess('수정되었습니다.');
 | 
				
			||||||
 | 
					            this.$router.push('/original-work');
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            this.notifyError('수정 실패');
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          const res = await createOriginal(this.form);
 | 
				
			||||||
 | 
					          if (res.status === 200 && res.data.success === true) {
 | 
				
			||||||
 | 
					            this.notifySuccess('등록되었습니다.');
 | 
				
			||||||
 | 
					            this.$router.push('/original-work');
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            this.notifyError('등록 실패');
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch (e) {
 | 
				
			||||||
 | 
					        this.notifyError(this.isEdit ? '수정 실패' : '등록 실패');
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style scoped>
 | 
				
			||||||
 | 
					.required-asterisk >>> .v-label::after { content: ' *'; color: #ff5252; }
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										205
									
								
								src/views/Chat/OriginalList.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										205
									
								
								src/views/Chat/OriginalList.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,205 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div>
 | 
				
			||||||
 | 
					    <v-toolbar dark>
 | 
				
			||||||
 | 
					      <v-spacer />
 | 
				
			||||||
 | 
					      <v-toolbar-title>원작 리스트</v-toolbar-title>
 | 
				
			||||||
 | 
					      <v-spacer />
 | 
				
			||||||
 | 
					      <v-btn
 | 
				
			||||||
 | 
					        color="primary"
 | 
				
			||||||
 | 
					        dark
 | 
				
			||||||
 | 
					        @click="goToCreate"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        원작 등록
 | 
				
			||||||
 | 
					      </v-btn>
 | 
				
			||||||
 | 
					    </v-toolbar>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <v-container>
 | 
				
			||||||
 | 
					      <v-row v-if="isLoading && originals.length === 0">
 | 
				
			||||||
 | 
					        <v-col class="text-center">
 | 
				
			||||||
 | 
					          <v-progress-circular
 | 
				
			||||||
 | 
					            indeterminate
 | 
				
			||||||
 | 
					            color="primary"
 | 
				
			||||||
 | 
					            size="64"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </v-col>
 | 
				
			||||||
 | 
					      </v-row>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <v-row>
 | 
				
			||||||
 | 
					        <v-col
 | 
				
			||||||
 | 
					          v-for="item in originals"
 | 
				
			||||||
 | 
					          :key="item.id"
 | 
				
			||||||
 | 
					          cols="12"
 | 
				
			||||||
 | 
					          sm="6"
 | 
				
			||||||
 | 
					          md="4"
 | 
				
			||||||
 | 
					          lg="3"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <v-card
 | 
				
			||||||
 | 
					            class="mx-auto"
 | 
				
			||||||
 | 
					            max-width="344"
 | 
				
			||||||
 | 
					            style="cursor:pointer;"
 | 
				
			||||||
 | 
					            @click="openDetail(item)"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <v-img
 | 
				
			||||||
 | 
					              :src="item.imageUrl"
 | 
				
			||||||
 | 
					              height="200"
 | 
				
			||||||
 | 
					              contain
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					            <v-card-title class="text-no-wrap">
 | 
				
			||||||
 | 
					              {{ item.title }}
 | 
				
			||||||
 | 
					            </v-card-title>
 | 
				
			||||||
 | 
					            <v-card-actions>
 | 
				
			||||||
 | 
					              <v-spacer />
 | 
				
			||||||
 | 
					              <v-btn
 | 
				
			||||||
 | 
					                small
 | 
				
			||||||
 | 
					                color="primary"
 | 
				
			||||||
 | 
					                @click.stop="editOriginal(item)"
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                수정
 | 
				
			||||||
 | 
					              </v-btn>
 | 
				
			||||||
 | 
					              <v-btn
 | 
				
			||||||
 | 
					                small
 | 
				
			||||||
 | 
					                color="error"
 | 
				
			||||||
 | 
					                @click.stop="confirmDelete(item)"
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                삭제
 | 
				
			||||||
 | 
					              </v-btn>
 | 
				
			||||||
 | 
					              <v-spacer />
 | 
				
			||||||
 | 
					            </v-card-actions>
 | 
				
			||||||
 | 
					          </v-card>
 | 
				
			||||||
 | 
					        </v-col>
 | 
				
			||||||
 | 
					      </v-row>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <v-row v-if="!isLoading && originals.length === 0">
 | 
				
			||||||
 | 
					        <v-col class="text-center">
 | 
				
			||||||
 | 
					          데이터가 없습니다.
 | 
				
			||||||
 | 
					        </v-col>
 | 
				
			||||||
 | 
					      </v-row>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <v-row v-if="isLoading && originals.length > 0">
 | 
				
			||||||
 | 
					        <v-col class="text-center">
 | 
				
			||||||
 | 
					          <v-progress-circular
 | 
				
			||||||
 | 
					            indeterminate
 | 
				
			||||||
 | 
					            color="primary"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </v-col>
 | 
				
			||||||
 | 
					      </v-row>
 | 
				
			||||||
 | 
					    </v-container>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <v-dialog
 | 
				
			||||||
 | 
					      v-model="deleteDialog"
 | 
				
			||||||
 | 
					      max-width="400"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <v-card>
 | 
				
			||||||
 | 
					        <v-card-title class="headline">
 | 
				
			||||||
 | 
					          삭제 확인
 | 
				
			||||||
 | 
					        </v-card-title>
 | 
				
			||||||
 | 
					        <v-card-text>정말 삭제하시겠습니까?</v-card-text>
 | 
				
			||||||
 | 
					        <v-card-actions>
 | 
				
			||||||
 | 
					          <v-spacer />
 | 
				
			||||||
 | 
					          <v-btn
 | 
				
			||||||
 | 
					            text
 | 
				
			||||||
 | 
					            @click="deleteDialog = false"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            취소
 | 
				
			||||||
 | 
					          </v-btn>
 | 
				
			||||||
 | 
					          <v-btn
 | 
				
			||||||
 | 
					            color="error"
 | 
				
			||||||
 | 
					            text
 | 
				
			||||||
 | 
					            @click="deleteItem"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            삭제
 | 
				
			||||||
 | 
					          </v-btn>
 | 
				
			||||||
 | 
					        </v-card-actions>
 | 
				
			||||||
 | 
					      </v-card>
 | 
				
			||||||
 | 
					    </v-dialog>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
					import { getOriginalList, deleteOriginal } from '@/api/original'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default {
 | 
				
			||||||
 | 
					  name: 'OriginalList',
 | 
				
			||||||
 | 
					  data() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      isLoading: false,
 | 
				
			||||||
 | 
					      originals: [],
 | 
				
			||||||
 | 
					      page: 1,
 | 
				
			||||||
 | 
					      hasMore: true,
 | 
				
			||||||
 | 
					      deleteDialog: false,
 | 
				
			||||||
 | 
					      selected: null
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  mounted() {
 | 
				
			||||||
 | 
					    this.loadMore();
 | 
				
			||||||
 | 
					    window.addEventListener('scroll', this.handleScroll);
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  beforeDestroy() {
 | 
				
			||||||
 | 
					    window.removeEventListener('scroll', this.handleScroll);
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  methods: {
 | 
				
			||||||
 | 
					    notifyError(message) { this.$dialog.notify.error(message) },
 | 
				
			||||||
 | 
					    notifySuccess(message) { this.$dialog.notify.success(message) },
 | 
				
			||||||
 | 
					    async loadMore() {
 | 
				
			||||||
 | 
					      if (this.isLoading || !this.hasMore) return;
 | 
				
			||||||
 | 
					      this.isLoading = true;
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const res = await getOriginalList(this.page);
 | 
				
			||||||
 | 
					        if (res.status === 200 && res.data.success === true) {
 | 
				
			||||||
 | 
					          const content = res.data.data?.content || [];
 | 
				
			||||||
 | 
					          this.originals = this.originals.concat(content);
 | 
				
			||||||
 | 
					          this.hasMore = content.length > 0;
 | 
				
			||||||
 | 
					          this.page++;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          this.notifyError('원작 목록 조회 실패');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch (e) {
 | 
				
			||||||
 | 
					        this.notifyError('원작 목록 조회 실패');
 | 
				
			||||||
 | 
					      } finally {
 | 
				
			||||||
 | 
					        this.isLoading = false;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    handleScroll() {
 | 
				
			||||||
 | 
					      const scrollPosition = window.innerHeight + window.scrollY;
 | 
				
			||||||
 | 
					      const documentHeight = document.documentElement.offsetHeight;
 | 
				
			||||||
 | 
					      if (scrollPosition >= documentHeight - 200 && !this.isLoading && this.hasMore) {
 | 
				
			||||||
 | 
					        this.loadMore();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    goToCreate() {
 | 
				
			||||||
 | 
					      this.$router.push('/original-work/form');
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    editOriginal(item) {
 | 
				
			||||||
 | 
					      this.$router.push({ path: '/original-work/form', query: { id: item.id } });
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    openDetail(item) {
 | 
				
			||||||
 | 
					      this.$router.push({ path: '/original-work/detail', query: { id: item.id } });
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    confirmDelete(item) {
 | 
				
			||||||
 | 
					      this.selected = item;
 | 
				
			||||||
 | 
					      this.deleteDialog = true;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    async deleteItem() {
 | 
				
			||||||
 | 
					      if (!this.selected) return;
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const res = await deleteOriginal(this.selected.id);
 | 
				
			||||||
 | 
					        if (res.status === 200 && res.data.success === true) {
 | 
				
			||||||
 | 
					          this.notifySuccess('삭제되었습니다.');
 | 
				
			||||||
 | 
					          this.originals = this.originals.filter(x => x.id !== this.selected.id);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          this.notifyError('삭제 실패');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch (e) {
 | 
				
			||||||
 | 
					        this.notifyError('삭제 실패');
 | 
				
			||||||
 | 
					      } finally {
 | 
				
			||||||
 | 
					        this.deleteDialog = false;
 | 
				
			||||||
 | 
					        this.selected = null;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style scoped>
 | 
				
			||||||
 | 
					.text-no-wrap { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
@@ -53,6 +53,24 @@
 | 
				
			|||||||
            <template v-slot:item.communitySettlementRatio="{ item }">
 | 
					            <template v-slot:item.communitySettlementRatio="{ item }">
 | 
				
			||||||
              {{ item.communitySettlementRatio }}%
 | 
					              {{ item.communitySettlementRatio }}%
 | 
				
			||||||
            </template>
 | 
					            </template>
 | 
				
			||||||
 | 
					            <template v-slot:item.actions="{ item }">
 | 
				
			||||||
 | 
					              <v-btn
 | 
				
			||||||
 | 
					                small
 | 
				
			||||||
 | 
					                color="primary"
 | 
				
			||||||
 | 
					                text
 | 
				
			||||||
 | 
					                @click="openEdit(item)"
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                수정
 | 
				
			||||||
 | 
					              </v-btn>
 | 
				
			||||||
 | 
					              <v-btn
 | 
				
			||||||
 | 
					                small
 | 
				
			||||||
 | 
					                color="red"
 | 
				
			||||||
 | 
					                text
 | 
				
			||||||
 | 
					                @click="confirmDelete(item)"
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                삭제
 | 
				
			||||||
 | 
					              </v-btn>
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
          </v-data-table>
 | 
					          </v-data-table>
 | 
				
			||||||
        </v-col>
 | 
					        </v-col>
 | 
				
			||||||
      </v-row>
 | 
					      </v-row>
 | 
				
			||||||
@@ -73,13 +91,20 @@
 | 
				
			|||||||
          persistent
 | 
					          persistent
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          <v-card>
 | 
					          <v-card>
 | 
				
			||||||
            <v-card-title>크리에이터 정산비율</v-card-title>
 | 
					            <v-card-title>{{ is_edit ? '크리에이터 정산비율 수정' : '크리에이터 정산비율' }}</v-card-title>
 | 
				
			||||||
            <v-card-text>
 | 
					            <v-card-text v-show="!is_edit">
 | 
				
			||||||
              <v-text-field
 | 
					              <v-text-field
 | 
				
			||||||
                v-model="creator_settlement_ratio.creator_id"
 | 
					                v-model="creator_settlement_ratio.creator_id"
 | 
				
			||||||
                label="크리에이터 번호"
 | 
					                label="크리에이터 번호"
 | 
				
			||||||
              />
 | 
					              />
 | 
				
			||||||
            </v-card-text>
 | 
					            </v-card-text>
 | 
				
			||||||
 | 
					            <v-card-text v-show="is_edit">
 | 
				
			||||||
 | 
					              <v-text-field
 | 
				
			||||||
 | 
					                v-model="creator_settlement_ratio.nickname"
 | 
				
			||||||
 | 
					                disabled
 | 
				
			||||||
 | 
					                label="크리에이터 닉네임"
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					            </v-card-text>
 | 
				
			||||||
            <v-card-text>
 | 
					            <v-card-text>
 | 
				
			||||||
              <v-text-field
 | 
					              <v-text-field
 | 
				
			||||||
                v-model="creator_settlement_ratio.subsidy"
 | 
					                v-model="creator_settlement_ratio.subsidy"
 | 
				
			||||||
@@ -118,7 +143,7 @@
 | 
				
			|||||||
                text
 | 
					                text
 | 
				
			||||||
                @click="validate"
 | 
					                @click="validate"
 | 
				
			||||||
              >
 | 
					              >
 | 
				
			||||||
                등록하기
 | 
					                {{ is_edit ? '수정하기' : '등록하기' }}
 | 
				
			||||||
              </v-btn>
 | 
					              </v-btn>
 | 
				
			||||||
            </v-card-actions>
 | 
					            </v-card-actions>
 | 
				
			||||||
          </v-card>
 | 
					          </v-card>
 | 
				
			||||||
@@ -142,6 +167,8 @@ export default {
 | 
				
			|||||||
      items: [],
 | 
					      items: [],
 | 
				
			||||||
      creator_settlement_ratio: {},
 | 
					      creator_settlement_ratio: {},
 | 
				
			||||||
      show_write_dialog: false,
 | 
					      show_write_dialog: false,
 | 
				
			||||||
 | 
					      is_edit: false,
 | 
				
			||||||
 | 
					      editing_item_id: null,
 | 
				
			||||||
      headers: [
 | 
					      headers: [
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          text: '닉네임',
 | 
					          text: '닉네임',
 | 
				
			||||||
@@ -173,6 +200,12 @@ export default {
 | 
				
			|||||||
          sortable: false,
 | 
					          sortable: false,
 | 
				
			||||||
          value: 'communitySettlementRatio',
 | 
					          value: 'communitySettlementRatio',
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          text: '관리',
 | 
				
			||||||
 | 
					          align: 'center',
 | 
				
			||||||
 | 
					          sortable: false,
 | 
				
			||||||
 | 
					          value: 'actions',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
      ],
 | 
					      ],
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
@@ -191,11 +224,16 @@ export default {
 | 
				
			|||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    showWriteDialog() {
 | 
					    showWriteDialog() {
 | 
				
			||||||
 | 
					      this.is_edit = false
 | 
				
			||||||
 | 
					      this.editing_item_id = null
 | 
				
			||||||
 | 
					      this.creator_settlement_ratio = {}
 | 
				
			||||||
      this.show_write_dialog = true
 | 
					      this.show_write_dialog = true
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    cancel() {
 | 
					    cancel() {
 | 
				
			||||||
      this.creator_settlement_ratio = {}
 | 
					      this.creator_settlement_ratio = {}
 | 
				
			||||||
 | 
					      this.is_edit = false
 | 
				
			||||||
 | 
					      this.editing_item_id = null
 | 
				
			||||||
      this.show_write_dialog = false
 | 
					      this.show_write_dialog = false
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -225,7 +263,11 @@ export default {
 | 
				
			|||||||
        return
 | 
					        return
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (this.is_edit) {
 | 
				
			||||||
 | 
					        this.updateCreatorSettlementRatio();
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
        this.createCreatorSettlementRatio();
 | 
					        this.createCreatorSettlementRatio();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async createCreatorSettlementRatio() {
 | 
					    async createCreatorSettlementRatio() {
 | 
				
			||||||
@@ -253,6 +295,71 @@ export default {
 | 
				
			|||||||
      this.is_loading = false
 | 
					      this.is_loading = false
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async updateCreatorSettlementRatio() {
 | 
				
			||||||
 | 
					      if (this.is_loading) return;
 | 
				
			||||||
 | 
					      this.is_loading = true
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        // 수정은 생성과 동일한 파라미터를 전송 (memberId 기준)
 | 
				
			||||||
 | 
					        const payload = { ...this.creator_settlement_ratio }
 | 
				
			||||||
 | 
					        const res = await api.updateCreatorSettlementRatio(payload)
 | 
				
			||||||
 | 
					        if (res.status === 200 && res.data.success === true) {
 | 
				
			||||||
 | 
					          this.cancel()
 | 
				
			||||||
 | 
					          this.notifySuccess(res.data.message || '수정되었습니다.')
 | 
				
			||||||
 | 
					          this.items = []
 | 
				
			||||||
 | 
					          await this.getSettlementRatio()
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          this.notifyError(res.data.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch (e) {
 | 
				
			||||||
 | 
					        this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
 | 
				
			||||||
 | 
					      } finally {
 | 
				
			||||||
 | 
					        this.is_loading = false
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    openEdit(item) {
 | 
				
			||||||
 | 
					      this.is_edit = true
 | 
				
			||||||
 | 
					      this.editing_item_id = null
 | 
				
			||||||
 | 
					      this.creator_settlement_ratio = {
 | 
				
			||||||
 | 
					        creator_id: item.memberId,
 | 
				
			||||||
 | 
					        nickname: item.nickname,
 | 
				
			||||||
 | 
					        subsidy: item.subsidy,
 | 
				
			||||||
 | 
					        liveSettlementRatio: item.liveSettlementRatio,
 | 
				
			||||||
 | 
					        contentSettlementRatio: item.contentSettlementRatio,
 | 
				
			||||||
 | 
					        communitySettlementRatio: item.communitySettlementRatio,
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      this.show_write_dialog = true
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async confirmDelete(item) {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const ok = await this.$dialog.confirm({ text: '삭제하시겠습니까?', title: '확인', actions: { false: '취소', true: '삭제' } })
 | 
				
			||||||
 | 
					        if (!ok) return
 | 
				
			||||||
 | 
					      } catch (e) {
 | 
				
			||||||
 | 
					        // 일부 구현체는 confirm이 boolean이 아닌 경우가 있음
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      this.deleteCreatorSettlementRatio(item)
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async deleteCreatorSettlementRatio(item) {
 | 
				
			||||||
 | 
					      if (this.is_loading) return;
 | 
				
			||||||
 | 
					      this.is_loading = true
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const memberId = item.memberId
 | 
				
			||||||
 | 
					        const res = await api.deleteCreatorSettlementRatio(memberId)
 | 
				
			||||||
 | 
					        if (res.status === 200 && res.data.success === true) {
 | 
				
			||||||
 | 
					          this.notifySuccess(res.data.message || '삭제되었습니다.')
 | 
				
			||||||
 | 
					          this.items = this.items.filter(x => (x.memberId) !== memberId)
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          this.notifyError(res.data.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch (e) {
 | 
				
			||||||
 | 
					        this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
 | 
				
			||||||
 | 
					      } finally {
 | 
				
			||||||
 | 
					        this.is_loading = false
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async getSettlementRatio() {
 | 
					    async getSettlementRatio() {
 | 
				
			||||||
      this.is_loading = true
 | 
					      this.is_loading = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -279,10 +386,6 @@ export default {
 | 
				
			|||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async next() {
 | 
					    async next() {
 | 
				
			||||||
      if (this.search_word.length < 2) {
 | 
					 | 
				
			||||||
        this.search_word = ''
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      await this.getSettlementRatio()
 | 
					      await this.getSettlementRatio()
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -49,6 +49,9 @@
 | 
				
			|||||||
                  <th class="text-center">
 | 
					                  <th class="text-center">
 | 
				
			||||||
                    회원타입
 | 
					                    회원타입
 | 
				
			||||||
                  </th>
 | 
					                  </th>
 | 
				
			||||||
 | 
					                  <th class="text-center">
 | 
				
			||||||
 | 
					                    로그인 타입
 | 
				
			||||||
 | 
					                  </th>
 | 
				
			||||||
                  <th class="text-center">
 | 
					                  <th class="text-center">
 | 
				
			||||||
                    OS
 | 
					                    OS
 | 
				
			||||||
                  </th>
 | 
					                  </th>
 | 
				
			||||||
@@ -92,6 +95,7 @@
 | 
				
			|||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
                  </td>
 | 
					                  </td>
 | 
				
			||||||
                  <td>{{ item.userType }}</td>
 | 
					                  <td>{{ item.userType }}</td>
 | 
				
			||||||
 | 
					                  <td>{{ item.loginType }}</td>
 | 
				
			||||||
                  <td>
 | 
					                  <td>
 | 
				
			||||||
                    <div v-if="item.container === 'aos'">
 | 
					                    <div v-if="item.container === 'aos'">
 | 
				
			||||||
                      Android
 | 
					                      Android
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -64,6 +64,15 @@
 | 
				
			|||||||
                <td>
 | 
					                <td>
 | 
				
			||||||
                  {{ total_sign_up_count }}
 | 
					                  {{ total_sign_up_count }}
 | 
				
			||||||
                </td>
 | 
					                </td>
 | 
				
			||||||
 | 
					                <td>
 | 
				
			||||||
 | 
					                  {{ total_sign_up_email_count }}
 | 
				
			||||||
 | 
					                </td>
 | 
				
			||||||
 | 
					                <td>
 | 
				
			||||||
 | 
					                  {{ total_sign_up_kakao_count }}
 | 
				
			||||||
 | 
					                </td>
 | 
				
			||||||
 | 
					                <td>
 | 
				
			||||||
 | 
					                  {{ total_sign_up_google_count }}
 | 
				
			||||||
 | 
					                </td>
 | 
				
			||||||
                <td>
 | 
					                <td>
 | 
				
			||||||
                  {{ total_auth_count }}
 | 
					                  {{ total_auth_count }}
 | 
				
			||||||
                </td>
 | 
					                </td>
 | 
				
			||||||
@@ -84,6 +93,18 @@
 | 
				
			|||||||
              {{ item.signUpCount.toLocaleString() }}
 | 
					              {{ item.signUpCount.toLocaleString() }}
 | 
				
			||||||
            </template>
 | 
					            </template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <template v-slot:item.signUpEmailCount="{ item }">
 | 
				
			||||||
 | 
					              {{ item.signUpEmailCount.toLocaleString() }}
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <template v-slot:item.signUpKakaoCount="{ item }">
 | 
				
			||||||
 | 
					              {{ item.signUpKakaoCount.toLocaleString() }}
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <template v-slot:item.signUpGoogleCount="{ item }">
 | 
				
			||||||
 | 
					              {{ item.signUpGoogleCount.toLocaleString() }}
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <template v-slot:item.authCount="{ item }">
 | 
					            <template v-slot:item.authCount="{ item }">
 | 
				
			||||||
              {{ item.authCount.toLocaleString() }}
 | 
					              {{ item.authCount.toLocaleString() }}
 | 
				
			||||||
            </template>
 | 
					            </template>
 | 
				
			||||||
@@ -127,6 +148,9 @@ export default {
 | 
				
			|||||||
      end_date: null,
 | 
					      end_date: null,
 | 
				
			||||||
      total_auth_count: 0,
 | 
					      total_auth_count: 0,
 | 
				
			||||||
      total_sign_up_count: 0,
 | 
					      total_sign_up_count: 0,
 | 
				
			||||||
 | 
					      total_sign_up_email_count: 0,
 | 
				
			||||||
 | 
					      total_sign_up_kakao_count: 0,
 | 
				
			||||||
 | 
					      total_sign_up_google_count: 0,
 | 
				
			||||||
      total_sign_out_count: 0,
 | 
					      total_sign_out_count: 0,
 | 
				
			||||||
      total_payment_member_count: 0,
 | 
					      total_payment_member_count: 0,
 | 
				
			||||||
      page: 1,
 | 
					      page: 1,
 | 
				
			||||||
@@ -145,6 +169,24 @@ export default {
 | 
				
			|||||||
          sortable: false,
 | 
					          sortable: false,
 | 
				
			||||||
          value: 'signUpCount',
 | 
					          value: 'signUpCount',
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          text: '이메일 가입 수',
 | 
				
			||||||
 | 
					          align: 'center',
 | 
				
			||||||
 | 
					          sortable: false,
 | 
				
			||||||
 | 
					          value: 'signUpEmailCount',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          text: '카카오 가입 수',
 | 
				
			||||||
 | 
					          align: 'center',
 | 
				
			||||||
 | 
					          sortable: false,
 | 
				
			||||||
 | 
					          value: 'signUpKakaoCount',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          text: '구글 가입 수',
 | 
				
			||||||
 | 
					          align: 'center',
 | 
				
			||||||
 | 
					          sortable: false,
 | 
				
			||||||
 | 
					          value: 'signUpGoogleCount',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          text: '본인인증 수',
 | 
					          text: '본인인증 수',
 | 
				
			||||||
          align: 'center',
 | 
					          align: 'center',
 | 
				
			||||||
@@ -208,6 +250,9 @@ export default {
 | 
				
			|||||||
          const data = res.data.data
 | 
					          const data = res.data.data
 | 
				
			||||||
          this.total_auth_count = data.totalAuthCount
 | 
					          this.total_auth_count = data.totalAuthCount
 | 
				
			||||||
          this.total_sign_up_count = data.totalSignUpCount
 | 
					          this.total_sign_up_count = data.totalSignUpCount
 | 
				
			||||||
 | 
					          this.total_sign_up_email_count = data.totalSignUpEmailCount
 | 
				
			||||||
 | 
					          this.total_sign_up_kakao_count = data.totalSignUpKakaoCount
 | 
				
			||||||
 | 
					          this.total_sign_up_google_count = data.totalSignUpGoogleCount
 | 
				
			||||||
          this.total_sign_out_count = data.totalSignOutCount
 | 
					          this.total_sign_out_count = data.totalSignOutCount
 | 
				
			||||||
          this.total_payment_member_count = data.totalPaymentMemberCount
 | 
					          this.total_payment_member_count = data.totalPaymentMemberCount
 | 
				
			||||||
          this.items = data.items
 | 
					          this.items = data.items
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,7 +14,7 @@
 | 
				
			|||||||
        <v-col>
 | 
					        <v-col>
 | 
				
			||||||
          <v-btn
 | 
					          <v-btn
 | 
				
			||||||
            block
 | 
					            block
 | 
				
			||||||
            color="#9970ff"
 | 
					            color="#3bb9f1"
 | 
				
			||||||
            dark
 | 
					            dark
 | 
				
			||||||
            depressed
 | 
					            depressed
 | 
				
			||||||
            @click="showWriteDialog"
 | 
					            @click="showWriteDialog"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -77,7 +77,7 @@
 | 
				
			|||||||
                  class="datepicker"
 | 
					                  class="datepicker"
 | 
				
			||||||
                  format="YYYY-MM-DD H:i"
 | 
					                  format="YYYY-MM-DD H:i"
 | 
				
			||||||
                />
 | 
					                />
 | 
				
			||||||
                <div> ~ </div>
 | 
					                <div> ~</div>
 | 
				
			||||||
                <datetime
 | 
					                <datetime
 | 
				
			||||||
                  v-model="event.endDate"
 | 
					                  v-model="event.endDate"
 | 
				
			||||||
                  class="datepicker"
 | 
					                  class="datepicker"
 | 
				
			||||||
@@ -281,16 +281,17 @@ import datetime from 'vuejs-datetimepicker';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
  name: "EventView",
 | 
					  name: "EventView",
 | 
				
			||||||
  components: { datetime },
 | 
					  components: {datetime},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  data() {
 | 
					  data() {
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      is_loading: false,
 | 
					      is_loading: false,
 | 
				
			||||||
      is_modify: false,
 | 
					      is_modify: false,
 | 
				
			||||||
      events: [],
 | 
					      events: [],
 | 
				
			||||||
      event: { isAdult: '' },
 | 
					      event: {isAdult: ''},
 | 
				
			||||||
      show_write_dialog: false,
 | 
					      show_write_dialog: false,
 | 
				
			||||||
      show_delete_confirm_dialog: false,
 | 
					      show_delete_confirm_dialog: false,
 | 
				
			||||||
 | 
					      selected_event: {},
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -361,6 +362,7 @@ export default {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    clickEvent(item) {
 | 
					    clickEvent(item) {
 | 
				
			||||||
      this.is_modify = true
 | 
					      this.is_modify = true
 | 
				
			||||||
 | 
					      this.selected_event = item
 | 
				
			||||||
      this.event.id = item.id
 | 
					      this.event.id = item.id
 | 
				
			||||||
      this.event.thumbnailImageUrl = item.thumbnailImageUrl
 | 
					      this.event.thumbnailImageUrl = item.thumbnailImageUrl
 | 
				
			||||||
      this.event.detailImageUrl = item.detailImageUrl
 | 
					      this.event.detailImageUrl = item.detailImageUrl
 | 
				
			||||||
@@ -376,7 +378,8 @@ export default {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    cancel() {
 | 
					    cancel() {
 | 
				
			||||||
      this.is_modify = false
 | 
					      this.is_modify = false
 | 
				
			||||||
      this.event = { isAdult: '' }
 | 
					      this.event = {isAdult: ''}
 | 
				
			||||||
 | 
					      this.selected_event = {}
 | 
				
			||||||
      this.show_write_dialog = false
 | 
					      this.show_write_dialog = false
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -440,7 +443,7 @@ export default {
 | 
				
			|||||||
          this.notifySuccess('등록되었습니다.')
 | 
					          this.notifySuccess('등록되었습니다.')
 | 
				
			||||||
          this.page = 1
 | 
					          this.page = 1
 | 
				
			||||||
          await this.getEvents()
 | 
					          await this.getEvents()
 | 
				
			||||||
          this.event = { isAdult: '' }
 | 
					          this.event = {isAdult: ''}
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      } catch (e) {
 | 
					      } catch (e) {
 | 
				
			||||||
        this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
 | 
					        this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
 | 
				
			||||||
@@ -457,7 +460,11 @@ export default {
 | 
				
			|||||||
        const formData = new FormData()
 | 
					        const formData = new FormData()
 | 
				
			||||||
        formData.append("id", this.event.id)
 | 
					        formData.append("id", this.event.id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (this.event.title != null && this.event.title.trim().length > 0) {
 | 
					        if (
 | 
				
			||||||
 | 
					          this.event.title != null &&
 | 
				
			||||||
 | 
					          this.event.title.trim().length > 0 &&
 | 
				
			||||||
 | 
					          this.selected_event.title !== this.event.title
 | 
				
			||||||
 | 
					        ) {
 | 
				
			||||||
          formData.append("title", this.event.title)
 | 
					          formData.append("title", this.event.title)
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -477,7 +484,7 @@ export default {
 | 
				
			|||||||
          formData.append("isPopup", this.event.isPopup)
 | 
					          formData.append("isPopup", this.event.isPopup)
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (this.event.link != null && this.event.link.trim().length > 0) {
 | 
					        if (this.selected_event.link !== this.event.link) {
 | 
				
			||||||
          formData.append("link", this.event.link)
 | 
					          formData.append("link", this.event.link)
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -485,11 +492,11 @@ export default {
 | 
				
			|||||||
          formData.append("isAdult", JSON.parse(this.event.isAdult))
 | 
					          formData.append("isAdult", JSON.parse(this.event.isAdult))
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (this.event.startDate != null) {
 | 
					        if (this.event.startDate != null && this.event.startDate !== this.selected_event.startDate) {
 | 
				
			||||||
          formData.append("startDate", this.event.startDate)
 | 
					          formData.append("startDate", this.event.startDate)
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (this.event.endDate != null) {
 | 
					        if (this.event.endDate != null && this.event.endDate !== this.selected_event.endDate) {
 | 
				
			||||||
          formData.append("endDate", this.event.endDate)
 | 
					          formData.append("endDate", this.event.endDate)
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -499,7 +506,7 @@ export default {
 | 
				
			|||||||
          this.notifySuccess('수정되었습니다.')
 | 
					          this.notifySuccess('수정되었습니다.')
 | 
				
			||||||
          this.page = 1
 | 
					          this.page = 1
 | 
				
			||||||
          await this.getEvents()
 | 
					          await this.getEvents()
 | 
				
			||||||
          this.event = { isAdult: '' }
 | 
					          this.event = {isAdult: ''}
 | 
				
			||||||
          this.is_modify = false
 | 
					          this.is_modify = false
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
          this.notifyError(res.data.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
 | 
					          this.notifyError(res.data.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										585
									
								
								src/views/Promotion/PointPolicyView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										585
									
								
								src/views/Promotion/PointPolicyView.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,585 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div>
 | 
				
			||||||
 | 
					    <v-toolbar dark>
 | 
				
			||||||
 | 
					      <v-spacer />
 | 
				
			||||||
 | 
					      <v-toolbar-title>포인트 정책</v-toolbar-title>
 | 
				
			||||||
 | 
					      <v-spacer />
 | 
				
			||||||
 | 
					    </v-toolbar>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <br>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <v-container>
 | 
				
			||||||
 | 
					      <v-row>
 | 
				
			||||||
 | 
					        <v-col cols="10" />
 | 
				
			||||||
 | 
					        <v-col>
 | 
				
			||||||
 | 
					          <v-btn
 | 
				
			||||||
 | 
					            block
 | 
				
			||||||
 | 
					            color="#3bb9f1"
 | 
				
			||||||
 | 
					            dark
 | 
				
			||||||
 | 
					            depressed
 | 
				
			||||||
 | 
					            @click="showWriteDialog"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            포인트 정책 등록
 | 
				
			||||||
 | 
					          </v-btn>
 | 
				
			||||||
 | 
					        </v-col>
 | 
				
			||||||
 | 
					      </v-row>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <v-row>
 | 
				
			||||||
 | 
					        <v-col>
 | 
				
			||||||
 | 
					          <v-data-table
 | 
				
			||||||
 | 
					            :headers="headers"
 | 
				
			||||||
 | 
					            :items="point_policy_list"
 | 
				
			||||||
 | 
					            :loading="is_loading"
 | 
				
			||||||
 | 
					            :items-per-page="-1"
 | 
				
			||||||
 | 
					            item-key="id"
 | 
				
			||||||
 | 
					            class="elevation-1"
 | 
				
			||||||
 | 
					            hide-default-footer
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <template v-slot:item.title="{ item }">
 | 
				
			||||||
 | 
					              {{ item.title }}
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <template v-slot:item.policyType="{ item }">
 | 
				
			||||||
 | 
					              {{ policy_type_map[item.policyType] }}
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <template v-slot:item.actionType="{ item }">
 | 
				
			||||||
 | 
					              {{ action_type_map[item.actionType] }}
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <template v-slot:item.threshold="{ item }">
 | 
				
			||||||
 | 
					              {{ item.threshold }} 번
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <template v-slot:item.availableCount="{ item }">
 | 
				
			||||||
 | 
					              {{ item.availableCount }} 번
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <template v-slot:item.period="{ item }">
 | 
				
			||||||
 | 
					              {{ item.startDate }} ~ {{ item.endDate }}
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <template v-slot:item.point="{ item }">
 | 
				
			||||||
 | 
					              {{ item.pointAmount }}
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <template v-slot:item.isActive="{ item }">
 | 
				
			||||||
 | 
					              <div v-if="item.isActive">
 | 
				
			||||||
 | 
					                O
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					              <div v-else>
 | 
				
			||||||
 | 
					                X
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <template v-slot:item.management="{ item }">
 | 
				
			||||||
 | 
					              <v-btn
 | 
				
			||||||
 | 
					                :disabled="is_loading"
 | 
				
			||||||
 | 
					                @click="showModifyDialog(item)"
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                수정
 | 
				
			||||||
 | 
					              </v-btn>
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					          </v-data-table>
 | 
				
			||||||
 | 
					        </v-col>
 | 
				
			||||||
 | 
					      </v-row>
 | 
				
			||||||
 | 
					      <v-row class="text-center">
 | 
				
			||||||
 | 
					        <v-col>
 | 
				
			||||||
 | 
					          <v-pagination
 | 
				
			||||||
 | 
					            v-model="page"
 | 
				
			||||||
 | 
					            :length="total_page"
 | 
				
			||||||
 | 
					            circle
 | 
				
			||||||
 | 
					            @input="next"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </v-col>
 | 
				
			||||||
 | 
					      </v-row>
 | 
				
			||||||
 | 
					    </v-container>
 | 
				
			||||||
 | 
					    <v-row>
 | 
				
			||||||
 | 
					      <v-dialog
 | 
				
			||||||
 | 
					        v-model="show_write_dialog"
 | 
				
			||||||
 | 
					        max-width="1000px"
 | 
				
			||||||
 | 
					        persistent
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <v-card>
 | 
				
			||||||
 | 
					          <v-card-title>포인트 정책 등록</v-card-title>
 | 
				
			||||||
 | 
					          <v-card-text>
 | 
				
			||||||
 | 
					            <v-text-field
 | 
				
			||||||
 | 
					              v-model="point_policy.title"
 | 
				
			||||||
 | 
					              label="제목"
 | 
				
			||||||
 | 
					              required
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          </v-card-text>
 | 
				
			||||||
 | 
					          <v-card-text v-if="selected_point_policy === null">
 | 
				
			||||||
 | 
					            <v-radio-group
 | 
				
			||||||
 | 
					              v-model="point_policy.policy_type"
 | 
				
			||||||
 | 
					              label="지급 유형 선택"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <v-radio
 | 
				
			||||||
 | 
					                v-for="item in policy_type_list"
 | 
				
			||||||
 | 
					                :key="item.value"
 | 
				
			||||||
 | 
					                :label="item.name"
 | 
				
			||||||
 | 
					                :value="item.value"
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					            </v-radio-group>
 | 
				
			||||||
 | 
					          </v-card-text>
 | 
				
			||||||
 | 
					          <v-card-text v-if="selected_point_policy === null">
 | 
				
			||||||
 | 
					            <v-radio-group
 | 
				
			||||||
 | 
					              v-model="point_policy.action_type"
 | 
				
			||||||
 | 
					              label="액션 선택"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <v-radio
 | 
				
			||||||
 | 
					                v-for="item in action_type_list"
 | 
				
			||||||
 | 
					                :key="item.value"
 | 
				
			||||||
 | 
					                :label="item.name"
 | 
				
			||||||
 | 
					                :value="item.value"
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					            </v-radio-group>
 | 
				
			||||||
 | 
					          </v-card-text>
 | 
				
			||||||
 | 
					          <v-card-text v-if="selected_point_policy === null">
 | 
				
			||||||
 | 
					            <v-text-field
 | 
				
			||||||
 | 
					              v-model="point_policy.threshold"
 | 
				
			||||||
 | 
					              label="참여해야 하는 횟수"
 | 
				
			||||||
 | 
					              required
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          </v-card-text>
 | 
				
			||||||
 | 
					          <v-card-text v-if="selected_point_policy === null">
 | 
				
			||||||
 | 
					            <v-text-field
 | 
				
			||||||
 | 
					              v-model="point_policy.point"
 | 
				
			||||||
 | 
					              label="포인트"
 | 
				
			||||||
 | 
					              required
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          </v-card-text>
 | 
				
			||||||
 | 
					          <v-card-text v-if="selected_point_policy === null">
 | 
				
			||||||
 | 
					            <v-text-field
 | 
				
			||||||
 | 
					              v-model="point_policy.available_count"
 | 
				
			||||||
 | 
					              label="참여 가능 횟수"
 | 
				
			||||||
 | 
					              required
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          </v-card-text>
 | 
				
			||||||
 | 
					          <v-card-text>
 | 
				
			||||||
 | 
					            <v-row align="center">
 | 
				
			||||||
 | 
					              <v-col cols="4">
 | 
				
			||||||
 | 
					                기간
 | 
				
			||||||
 | 
					              </v-col>
 | 
				
			||||||
 | 
					              <v-col
 | 
				
			||||||
 | 
					                cols="8"
 | 
				
			||||||
 | 
					                class="datepicker-wrapper"
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <datetime
 | 
				
			||||||
 | 
					                  v-model="point_policy.start_date"
 | 
				
			||||||
 | 
					                  class="datepicker"
 | 
				
			||||||
 | 
					                  format="YYYY-MM-DD H:i"
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					                <div> ~</div>
 | 
				
			||||||
 | 
					                <datetime
 | 
				
			||||||
 | 
					                  v-model="point_policy.end_date"
 | 
				
			||||||
 | 
					                  class="datepicker"
 | 
				
			||||||
 | 
					                  format="YYYY-MM-DD H:i"
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              </v-col>
 | 
				
			||||||
 | 
					            </v-row>
 | 
				
			||||||
 | 
					          </v-card-text>
 | 
				
			||||||
 | 
					          <v-card-text v-show="selected_point_policy !== null">
 | 
				
			||||||
 | 
					            <v-row align="center">
 | 
				
			||||||
 | 
					              <v-col cols="4">
 | 
				
			||||||
 | 
					                활성화
 | 
				
			||||||
 | 
					              </v-col>
 | 
				
			||||||
 | 
					              <v-col cols="8">
 | 
				
			||||||
 | 
					                <input
 | 
				
			||||||
 | 
					                  v-model="point_policy.is_active"
 | 
				
			||||||
 | 
					                  type="checkbox"
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					              </v-col>
 | 
				
			||||||
 | 
					            </v-row>
 | 
				
			||||||
 | 
					          </v-card-text>
 | 
				
			||||||
 | 
					          <v-card-actions v-show="!is_loading">
 | 
				
			||||||
 | 
					            <v-spacer />
 | 
				
			||||||
 | 
					            <v-btn
 | 
				
			||||||
 | 
					              color="blue darken-1"
 | 
				
			||||||
 | 
					              text
 | 
				
			||||||
 | 
					              @click="cancel"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              취소
 | 
				
			||||||
 | 
					            </v-btn>
 | 
				
			||||||
 | 
					            <v-btn
 | 
				
			||||||
 | 
					              v-if="selected_point_policy !== null"
 | 
				
			||||||
 | 
					              color="blue darken-1"
 | 
				
			||||||
 | 
					              text
 | 
				
			||||||
 | 
					              @click="modify"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              수정
 | 
				
			||||||
 | 
					            </v-btn>
 | 
				
			||||||
 | 
					            <v-btn
 | 
				
			||||||
 | 
					              v-else
 | 
				
			||||||
 | 
					              color="blue darken-1"
 | 
				
			||||||
 | 
					              text
 | 
				
			||||||
 | 
					              @click="validate"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              등록
 | 
				
			||||||
 | 
					            </v-btn>
 | 
				
			||||||
 | 
					          </v-card-actions>
 | 
				
			||||||
 | 
					        </v-card>
 | 
				
			||||||
 | 
					      </v-dialog>
 | 
				
			||||||
 | 
					    </v-row>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
					import * as api from '@/api/point_policy'
 | 
				
			||||||
 | 
					import datetime from "vuejs-datetimepicker";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default {
 | 
				
			||||||
 | 
					  name: "PointPolicyView",
 | 
				
			||||||
 | 
					  components: {datetime},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  data() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      is_loading: false,
 | 
				
			||||||
 | 
					      show_write_dialog: false,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      action_type_list: [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          name: '본인인증',
 | 
				
			||||||
 | 
					          value: 'USER_AUTHENTICATION'
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          name: '콘텐츠 댓글',
 | 
				
			||||||
 | 
					          value: 'CONTENT_COMMENT'
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          name: '구매한 콘텐츠 댓글',
 | 
				
			||||||
 | 
					          value: 'ORDER_CONTENT_COMMENT'
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          name: '라이브 연속 청취 30분',
 | 
				
			||||||
 | 
					          value: 'LIVE_CONTINUOUS_LISTEN_30'
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      action_type_map: {
 | 
				
			||||||
 | 
					        'USER_AUTHENTICATION': '본인인증',
 | 
				
			||||||
 | 
					        'CONTENT_COMMENT': '콘텐츠 댓글',
 | 
				
			||||||
 | 
					        'ORDER_CONTENT_COMMENT': '구매한 콘텐츠 댓글',
 | 
				
			||||||
 | 
					        'LIVE_CONTINUOUS_LISTEN_30': '라이브 연속 청취 30분',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      policy_type_list: [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          name: '매일',
 | 
				
			||||||
 | 
					          value: 'DAILY'
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          name: '전체',
 | 
				
			||||||
 | 
					          value: 'TOTAL'
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      policy_type_map: {
 | 
				
			||||||
 | 
					        'DAILY': '매일',
 | 
				
			||||||
 | 
					        'TOTAL': '전체',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      point_policy: {
 | 
				
			||||||
 | 
					        title: '',
 | 
				
			||||||
 | 
					        policy_type: '',
 | 
				
			||||||
 | 
					        action_type: '',
 | 
				
			||||||
 | 
					        threshold: 0,
 | 
				
			||||||
 | 
					        available_count: 0,
 | 
				
			||||||
 | 
					        point: 0,
 | 
				
			||||||
 | 
					        start_date: '',
 | 
				
			||||||
 | 
					        end_date: ''
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      selected_point_policy: null,
 | 
				
			||||||
 | 
					      point_policy_list: [],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      page: 1,
 | 
				
			||||||
 | 
					      total_page: 0,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      headers: [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          text: '제목',
 | 
				
			||||||
 | 
					          align: 'center',
 | 
				
			||||||
 | 
					          sortable: false,
 | 
				
			||||||
 | 
					          value: 'title',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          text: '지급유형',
 | 
				
			||||||
 | 
					          align: 'center',
 | 
				
			||||||
 | 
					          sortable: false,
 | 
				
			||||||
 | 
					          value: 'policyType',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          text: '액션',
 | 
				
			||||||
 | 
					          align: 'center',
 | 
				
			||||||
 | 
					          sortable: false,
 | 
				
			||||||
 | 
					          value: 'actionType',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          text: '참여해야 하는 횟수',
 | 
				
			||||||
 | 
					          align: 'center',
 | 
				
			||||||
 | 
					          sortable: false,
 | 
				
			||||||
 | 
					          value: 'threshold',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          text: '참여 가능 횟수',
 | 
				
			||||||
 | 
					          align: 'center',
 | 
				
			||||||
 | 
					          sortable: false,
 | 
				
			||||||
 | 
					          value: 'availableCount',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          text: '기간',
 | 
				
			||||||
 | 
					          align: 'center',
 | 
				
			||||||
 | 
					          sortable: false,
 | 
				
			||||||
 | 
					          value: 'period',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          text: '포인트',
 | 
				
			||||||
 | 
					          align: 'center',
 | 
				
			||||||
 | 
					          sortable: false,
 | 
				
			||||||
 | 
					          value: 'pointAmount',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          text: '활성화',
 | 
				
			||||||
 | 
					          align: 'center',
 | 
				
			||||||
 | 
					          sortable: false,
 | 
				
			||||||
 | 
					          value: 'isActive',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          text: '관리',
 | 
				
			||||||
 | 
					          align: 'center',
 | 
				
			||||||
 | 
					          sortable: false,
 | 
				
			||||||
 | 
					          value: 'management'
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async created() {
 | 
				
			||||||
 | 
					    await this.getPointPolicyList()
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  methods: {
 | 
				
			||||||
 | 
					    notifyError(message) {
 | 
				
			||||||
 | 
					      this.$dialog.notify.error(message)
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    notifySuccess(message) {
 | 
				
			||||||
 | 
					      this.$dialog.notify.success(message)
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    showWriteDialog() {
 | 
				
			||||||
 | 
					      this.show_write_dialog = true
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    showModifyDialog(item) {
 | 
				
			||||||
 | 
					      this.selected_point_policy = item;
 | 
				
			||||||
 | 
					      this.point_policy = {
 | 
				
			||||||
 | 
					        title: item.title,
 | 
				
			||||||
 | 
					        policy_type: item.policyType,
 | 
				
			||||||
 | 
					        action_type: item.actionType,
 | 
				
			||||||
 | 
					        threshold: item.threshold,
 | 
				
			||||||
 | 
					        available_count: item.availableCount,
 | 
				
			||||||
 | 
					        point: item.pointAmount,
 | 
				
			||||||
 | 
					        start_date: item.startDate,
 | 
				
			||||||
 | 
					        end_date: item.endDate,
 | 
				
			||||||
 | 
					        is_active: item.isActive
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					      this.show_write_dialog = true
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    validate() {
 | 
				
			||||||
 | 
					      if (this.point_policy.title.trim() === '') {
 | 
				
			||||||
 | 
					        this.notifyError('제목을 입력하세요.')
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (this.point_policy.policy_type.trim() === '') {
 | 
				
			||||||
 | 
					        this.notifyError('지급유형을 선택하세요')
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (this.point_policy.action_type.trim() === '') {
 | 
				
			||||||
 | 
					        this.notifyError('액션을 선택하세요')
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (isNaN(this.point_policy.threshold)) {
 | 
				
			||||||
 | 
					        this.notifyError('참여 해야하는 횟수는 숫자만 입력 가능합니다.')
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (this.point_policy.threshold <= 0) {
 | 
				
			||||||
 | 
					        this.notifyError('참여 해야하는 횟수는 1이상 입력 가능합니다.')
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (isNaN(this.point_policy.point)) {
 | 
				
			||||||
 | 
					        this.notifyError('지급 포인트는 숫자만 입력 가능합니다.')
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (isNaN(this.point_policy.available_count)) {
 | 
				
			||||||
 | 
					        this.notifyError('참여 가능 횟수는 숫자만 입력 가능합니다.')
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (this.point_policy.available_count <= 0) {
 | 
				
			||||||
 | 
					        this.notifyError('참여 가능 횟수는 1이상 입력 가능합니다.')
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (this.point_policy.start_date.trim() === '') {
 | 
				
			||||||
 | 
					        this.notifyError('정책 시작 날짜를 입력하세요')
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      this.submit()
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    cancel() {
 | 
				
			||||||
 | 
					      this.point_policy = {
 | 
				
			||||||
 | 
					        title: '',
 | 
				
			||||||
 | 
					        policy_type: '',
 | 
				
			||||||
 | 
					        action_type: '',
 | 
				
			||||||
 | 
					        threshold: 0,
 | 
				
			||||||
 | 
					        available_count: 0,
 | 
				
			||||||
 | 
					        point: 0,
 | 
				
			||||||
 | 
					        start_date: '',
 | 
				
			||||||
 | 
					        end_date: ''
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      this.show_write_dialog = false
 | 
				
			||||||
 | 
					      this.selected_point_policy = null;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async submit() {
 | 
				
			||||||
 | 
					      if (this.is_loading) return;
 | 
				
			||||||
 | 
					      this.is_loading = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const request = {
 | 
				
			||||||
 | 
					          'title': this.point_policy.title,
 | 
				
			||||||
 | 
					          'policyType': this.point_policy.policy_type,
 | 
				
			||||||
 | 
					          'actionType': this.point_policy.action_type,
 | 
				
			||||||
 | 
					          'threshold': this.point_policy.threshold,
 | 
				
			||||||
 | 
					          'availableCount': this.point_policy.available_count,
 | 
				
			||||||
 | 
					          'pointAmount': this.point_policy.point,
 | 
				
			||||||
 | 
					          'startDate': this.point_policy.start_date,
 | 
				
			||||||
 | 
					          'endDate': this.point_policy.end_date
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        const res = await api.createPointPolicyList(request)
 | 
				
			||||||
 | 
					        this.is_loading = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (res.status === 200 && res.data.success === true) {
 | 
				
			||||||
 | 
					          this.cancel()
 | 
				
			||||||
 | 
					          this.notifySuccess(res.data.message || '등록되었습니다.')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          this.page = 1
 | 
				
			||||||
 | 
					          this.point_policy_list = []
 | 
				
			||||||
 | 
					          await this.getPointPolicyList()
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          this.notifyError(res.data.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch (e) {
 | 
				
			||||||
 | 
					        this.notifyError("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async modify() {
 | 
				
			||||||
 | 
					      if (this.is_loading) return;
 | 
				
			||||||
 | 
					      this.is_loading = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const request = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (this.point_policy.title !== this.selected_point_policy.title) {
 | 
				
			||||||
 | 
					          request.title = this.point_policy.title
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (this.point_policy.start_date !== this.selected_point_policy.startDate) {
 | 
				
			||||||
 | 
					          request.startDate = this.point_policy.start_date
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (this.point_policy.end_date !== this.selected_point_policy.endDate) {
 | 
				
			||||||
 | 
					          request.endDate = this.point_policy.end_date
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (this.point_policy.is_active !== this.selected_point_policy.isActive) {
 | 
				
			||||||
 | 
					          request.isActive = this.point_policy.is_active
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const res = await api.updatePointPolicyList(this.selected_point_policy.id, request)
 | 
				
			||||||
 | 
					        this.is_loading = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (res.status === 200 && res.data.success === true) {
 | 
				
			||||||
 | 
					          this.cancel()
 | 
				
			||||||
 | 
					          this.notifySuccess(res.data.message || '수정되었습니다.')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          this.point_policy_list = []
 | 
				
			||||||
 | 
					          await this.getPointPolicyList()
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          this.notifyError(res.data.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch (e) {
 | 
				
			||||||
 | 
					        this.notifyError("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async getPointPolicyList() {
 | 
				
			||||||
 | 
					      if (this.is_loading) return;
 | 
				
			||||||
 | 
					      this.is_loading = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const res = await api.getPointPolicyList(this.page);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (res.status === 200 && res.data.success === true) {
 | 
				
			||||||
 | 
					          const data = res.data.data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          const total_page = Math.ceil(data.totalCount / 20)
 | 
				
			||||||
 | 
					          this.point_policy_list = data.items
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (total_page <= 0)
 | 
				
			||||||
 | 
					            this.total_page = 1
 | 
				
			||||||
 | 
					          else
 | 
				
			||||||
 | 
					            this.total_page = total_page
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          this.notifyError(res.data.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch (e) {
 | 
				
			||||||
 | 
					        this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      this.is_loading = false
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async next() {
 | 
				
			||||||
 | 
					      await this.getPointPolicyList()
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style scoped>
 | 
				
			||||||
 | 
					.datepicker {
 | 
				
			||||||
 | 
					  text-align: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.datepicker-wrapper {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: row;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.datepicker-wrapper > div {
 | 
				
			||||||
 | 
					  margin: 20px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.v-card__text {
 | 
				
			||||||
 | 
					  margin-top: 20px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.v-card__actions {
 | 
				
			||||||
 | 
					  margin-top: 100px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.v-card__actions > .v-btn {
 | 
				
			||||||
 | 
					  font-size: 20px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
		Reference in New Issue
	
	Block a user