퀘스트 강제 완료

경제지표 재화 헤더 스타일 변경
This commit is contained in:
2025-07-18 15:18:45 +09:00
parent 26114c9a9b
commit 99943c0b19
6 changed files with 166 additions and 41 deletions

View File

@@ -185,6 +185,21 @@ export const UserQuestView = async (token, guid) => {
}
};
//퀘스트 테스크 완료
export const UserQuestTaskComplete = async (token, params) => {
try {
const res = await Axios.post(`/api/v1/users/quest/task`, params, {
headers: { Authorization: `Bearer ${token}` },
});
return res.data;
} catch (e) {
if (e instanceof Error) {
throw new Error('UserQuestTaskComplete Error', e);
}
}
};
// 친구목록 조회
export const UserFriendListView = async (token, guid) => {
try {

View File

@@ -110,6 +110,11 @@ export const questStatus = [
{ value: 'RUNNING', name: '진행중' },
];
export const questCompleteStatusType = [
{ value: 0, name: '미완료' },
{ value: 1, name: '완료' }
]
export const currencyItemCode = [
{ value: '19010001', name: '골드' },
{ value: '19010002', name: '사파이어' },

View File

@@ -5,15 +5,31 @@ import { BtnWrapper, TableStyle } from '../../styles/Components';
import Button from '../../components/common/button/Button';
import Modal from '../../components/common/modal/Modal';
import { useEffect, useState, Fragment } from 'react';
import { questStatus } from '../../assets/data/options';
import { commonStatus } from '../../assets/data';
import { useModal } from '../../hooks/hook';
import { alertTypes } from '../../assets/data/types';
import { useAlert } from '../../context/AlertProvider';
const QuestDetailModal = ({ detailPop, handleClick, detailQuest, handleQuestComplete }) => {
const { showModal } = useAlert();
const QuestDetailModal = ({ detailPop, handleClick, detailQuest }) => {
const [detailList, setDetailList] = useState([])
useEffect(() => {
Array.isArray(detailQuest) && setDetailList(detailQuest)
Array.isArray(detailQuest.detailQuest) && setDetailList(detailQuest.detailQuest)
}, [detailQuest])
// const questlist = [{ taskNo: detailQuest.task_no, taskName: detailQuest.quest_name, counter: detailQuest.counter, state: detailQuest.status }];
const handleQuestCompleteConfirm = (data) => {
const params = {...data, quest_key: detailQuest.quest_key}
showModal('QUEST_TASK_COMPLETE_CONFIRM',{
type: alertTypes.confirm,
onConfirm: () => {
handleQuestComplete(params);
}
});
}
return (
<>
<Modal $view={detailPop} min="480px">
@@ -25,7 +41,8 @@ const QuestDetailModal = ({ detailPop, handleClick, detailQuest }) => {
<th width="80">Task No</th>
<th>Task Name</th>
<th width="120">Counter</th>
<th width="120">State</th>
<th width="120">상태</th>
<th width="120">완료처리</th>
</tr>
</thead>
<tbody>
@@ -36,7 +53,10 @@ const QuestDetailModal = ({ detailPop, handleClick, detailQuest }) => {
<td>{el.task_no}</td>
<td>{el.quest_name}</td>
<td>{el.counter}</td>
<td>{el.status}</td>
<td>{questStatus.find(data => data.value === el.status)?.name}</td>
<td>
{ el.status === commonStatus.running && <Button text="완료" theme="line" handleClick={() => handleQuestCompleteConfirm(el)} />}
</td>
</tr>
</Fragment>
);

View File

@@ -3,15 +3,22 @@ import { useState, useEffect, Fragment } from 'react';
import styled from 'styled-components';
import Button from '../../components/common/button/Button';
import QuestDetailModal from '../../components/DataManage/QuestDetailModal';
import { UserQuestView } from '../../apis/Users';
import { UserQuestTaskComplete, UserQuestView } from '../../apis/Users';
import { convertKTC } from '../../utils';
import { TableSkeleton } from '../Skeleton/TableSkeleton';
import { useAlert } from '../../context/AlertProvider';
import { CaliumCharge } from '../../apis';
import { alertTypes } from '../../assets/data/types';
import { useLoading } from '../../context/LoadingProvider';
import { questCompleteStatusType } from '../../assets/data/options';
const UserQuestInfo = ({ userInfo }) => {
const [detailPop, setDetailPop] = useState('hidden');
const [dataList, setDataList] = useState({});
const [detailQuest, setDetailQuest] = useState({});
const [loading, setLoading] = useState(true);
const { showModal, showToast } = useAlert();
const { withLoading } = useLoading();
useEffect(() => {
if(userInfo && Object.keys(userInfo).length > 0) {
@@ -30,10 +37,30 @@ const UserQuestInfo = ({ userInfo }) => {
const handleClick = data => {
if (detailPop === 'hidden') {
setDetailPop('view');
setDetailQuest(data.detailQuest);
setDetailQuest(data);
} else setDetailPop('hidden');
};
const handleQuestComplete = async data => {
const token = sessionStorage.getItem('token');
await withLoading(async () => {
const params = {...data, guid: userInfo.guid};
return await UserQuestTaskComplete(token, params);
}).then(data => {
if (data.result === "SUCCESS") {
showToast('QUEST_TASK_COMPLETE', { type: alertTypes.success });
} else {
showToast(data.data.message, { type: alertTypes.error });
}
}).catch(error => {
showToast('API_FAIL', { type: alertTypes.error });
}).finally(() => {
handleClick();
fetchData();
});
};
return (
loading ? <TableSkeleton /> :
<>
@@ -59,7 +86,7 @@ const UserQuestInfo = ({ userInfo }) => {
<td>{el.quest_id}</td>
<td>{el.quest_name}</td>
<td>{el.quest_type}</td>
<td>{el.status}</td>
<td>{questCompleteStatusType.find(data => el.status === data.value).name || el.status}</td>
<td>{convertKTC(el.quest_assign_time, false)}</td>
<td>{convertKTC(el.quest_complete_time, false)}</td>
<td>
@@ -72,7 +99,7 @@ const UserQuestInfo = ({ userInfo }) => {
</tbody>
</QuestTable>
</UserTableWrapper>
<QuestDetailModal detailPop={detailPop} handleClick={handleClick} detailQuest={detailQuest} />
<QuestDetailModal detailPop={detailPop} handleClick={handleClick} detailQuest={detailQuest} handleQuestComplete={handleQuestComplete} />
</>
);
};

View File

@@ -39,30 +39,47 @@ const CreditContent = () => {
const tableHeaders = useMemo(() => {
return [
{ id: 'logDay', label: '일자', width: '100px' },
{ id: 'accountId', label: 'account ID', width: '80px' },
{ id: 'userGuid', label: 'GUID', width: '200px' },
{ id: 'userNickname', label: '아바타명', width: '150px' },
{ id: 'sapphireAcquired', label: '사파이어 획득량', width: '80px' },
{ id: 'sapphireConsumed', label: '사파이어 소모량', width: '80px' },
{ id: 'goldAcquired', label: '골드 획득량', width: '80px' },
{ id: 'goldConsumed', label: '골드 소모량', width: '80px' },
{ id: 'caliumAcquired', label: '칼리움 획득량', width: '80px' },
{ id: 'caliumConsumed', label: '칼리움 소모량', width: '80px' },
{ id: 'beamAcquired', label: 'BEAM 획득량', width: '80px' },
{ id: 'beamConsumed', label: 'BEAM 소모량', width: '80px' },
{ id: 'rubyAcquired', label: '루비 획득량', width: '80px' },
{ id: 'rubyConsumed', label: '루비 소모량', width: '80px' },
{ id: 'sapphireNet', label: '사파이어 계', width: '80px' },
{ id: 'goldNet', label: '골드 계', width: '80px' },
{ id: 'caliumNet', label: '칼리움 계', width: '80px' },
{ id: 'beamNet', label: 'BEAM 계', width: '80px' },
{ id: 'rubyNet', label: '루비 계', width: '80px' },
{ id: 'totalCurrencies', label: '활동 수', width: '80px' },
{ id: 'detail', label: '상세', width: '100px' },
// 기본 컬럼 (rowSpan=2)
{ id: 'logDay', label: '일자', width: '100px', rowSpan: 2 },
{ id: 'accountId', label: 'account ID', width: '80px', rowSpan: 2 },
{ id: 'userGuid', label: 'GUID', width: '200px', rowSpan: 2 },
{ id: 'userNickname', label: '아바타명', width: '150px', rowSpan: 2 },
// 획득량 그룹 헤더 (첫 번째 행에만 표시)
{ id: 'acquired', label: '획득', width: '400px', colSpan: 5, groupHeader: true },
// 획득량 컬럼 (두 번째 행에만 표시)
{ id: 'sapphireAcquired', label: '사파이어', width: '80px', groupRow: true },
{ id: 'goldAcquired', label: '골드', width: '80px', groupRow: true },
{ id: 'caliumAcquired', label: '칼리움', width: '80px', groupRow: true },
{ id: 'beamAcquired', label: 'BEAM', width: '80px', groupRow: true },
{ id: 'rubyAcquired', label: '루비', width: '80px', groupRow: true },
// 소모량 그룹 헤더 (첫 번째 행에만 표시)
{ id: 'consumed', label: '소모', width: '400px', colSpan: 5, groupHeader: true },
// 소모량 컬럼 (두 번째 행에만 표시)
{ id: 'sapphireConsumed', label: '사파이어', width: '80px', groupRow: true },
{ id: 'goldConsumed', label: '골드', width: '80px', groupRow: true },
{ id: 'caliumConsumed', label: '칼리움', width: '80px', groupRow: true },
{ id: 'beamConsumed', label: 'BEAM', width: '80px', groupRow: true },
{ id: 'rubyConsumed', label: '루비', width: '80px', groupRow: true },
// 계 컬럼 (rowSpan=2)
{ id: 'sapphireNet', label: '사파이어 계', width: '80px', rowSpan: 2 },
{ id: 'goldNet', label: '골드 계', width: '80px', rowSpan: 2 },
{ id: 'caliumNet', label: '칼리움 계', width: '80px', rowSpan: 2 },
{ id: 'beamNet', label: 'BEAM 계', width: '80px', rowSpan: 2 },
{ id: 'rubyNet', label: '루비 계', width: '80px', rowSpan: 2 },
// 기타 컬럼 (rowSpan=2)
{ id: 'totalCurrencies', label: '활동 수', width: '80px', rowSpan: 2 },
{ id: 'detail', label: '상세', width: '100px', rowSpan: 2 }
];
}, []);
const totals = useMemo(() => {
if (!dataList?.currency_list?.length) return null;
@@ -151,30 +168,62 @@ const CreditContent = () => {
<TableStyle ref={tableRef}>
<thead>
<tr>
{tableHeaders.map(header => (
<th key={header.id} width={header.width}>{header.label}</th>
))}
{/* 첫 번째 행 - 기본 컬럼 + 그룹 헤더 + rowSpan=2 컬럼 */}
{tableHeaders.map(header => {
if (header.groupRow) return null; // 두 번째 행의 컬럼은 첫 번째 행에서 건너뜀
return (
<th
key={header.id}
width={header.width}
rowSpan={header.rowSpan}
colSpan={header.colSpan}
>
{header.label}
</th>
);
})}
</tr>
<tr>
{/* 두 번째 행 - 그룹 내 하위 컬럼만 */}
{tableHeaders.map(header => {
if (!header.groupRow) return null; // 첫 번째 행이나 rowSpan=2 컬럼은 두 번째 행에서 건너뜀
return (
<th key={header.id} width={header.width}>
{header.label}
</th>
);
})}
</tr>
</thead>
<tbody>
{totals && (
<TotalRow>
<td colSpan="4">합계</td>
{/* 획득 그룹 합계 */}
<td>{numberFormatter.formatCurrency(totals.sapphireAcquired)}</td>
<td>{numberFormatter.formatCurrency(totals.sapphireConsumed)}</td>
<td>{numberFormatter.formatCurrency(totals.goldAcquired)}</td>
<td>{numberFormatter.formatCurrency(totals.goldConsumed)}</td>
<td>{numberFormatter.formatCurrency(totals.caliumAcquired)}</td>
<td>{numberFormatter.formatCurrency(totals.caliumConsumed)}</td>
<td>{numberFormatter.formatCurrency(totals.beamAcquired)}</td>
<td>{numberFormatter.formatCurrency(totals.beamConsumed)}</td>
<td>{numberFormatter.formatCurrency(totals.rubyAcquired)}</td>
{/* 소모 그룹 합계 */}
<td>{numberFormatter.formatCurrency(totals.sapphireConsumed)}</td>
<td>{numberFormatter.formatCurrency(totals.goldConsumed)}</td>
<td>{numberFormatter.formatCurrency(totals.caliumConsumed)}</td>
<td>{numberFormatter.formatCurrency(totals.beamConsumed)}</td>
<td>{numberFormatter.formatCurrency(totals.rubyConsumed)}</td>
{/* 계 합계 */}
<td>{numberFormatter.formatCurrency(totals.sapphireNet)}</td>
<td>{numberFormatter.formatCurrency(totals.goldNet)}</td>
<td>{numberFormatter.formatCurrency(totals.caliumNet)}</td>
<td>{numberFormatter.formatCurrency(totals.beamNet)}</td>
<td>{numberFormatter.formatCurrency(totals.rubyNet)}</td>
<td>{totals.totalCurrencies}</td>
<td>-</td>
</TotalRow>
@@ -182,25 +231,33 @@ const CreditContent = () => {
{dataList?.currency_list?.map((item, index) => (
<Fragment key={index}>
<tr>
{/* 기본 정보 */}
<td>{item.logDay}</td>
<td>{item.accountId}</td>
<td>{item.userGuid}</td>
<td>{item.userNickname}</td>
{/* 획득 그룹 */}
<td>{numberFormatter.formatCurrency(item.sapphireAcquired)}</td>
<td>{numberFormatter.formatCurrency(item.sapphireConsumed)}</td>
<td>{numberFormatter.formatCurrency(item.goldAcquired)}</td>
<td>{numberFormatter.formatCurrency(item.goldConsumed)}</td>
<td>{numberFormatter.formatCurrency(item.caliumAcquired)}</td>
<td>{numberFormatter.formatCurrency(item.caliumConsumed)}</td>
<td>{numberFormatter.formatCurrency(item.beamAcquired)}</td>
<td>{numberFormatter.formatCurrency(item.beamConsumed)}</td>
<td>{numberFormatter.formatCurrency(item.rubyAcquired)}</td>
{/* 소모 그룹 */}
<td>{numberFormatter.formatCurrency(item.sapphireConsumed)}</td>
<td>{numberFormatter.formatCurrency(item.goldConsumed)}</td>
<td>{numberFormatter.formatCurrency(item.caliumConsumed)}</td>
<td>{numberFormatter.formatCurrency(item.beamConsumed)}</td>
<td>{numberFormatter.formatCurrency(item.rubyConsumed)}</td>
{/* 계 */}
<td>{numberFormatter.formatCurrency(item.sapphireNet)}</td>
<td>{numberFormatter.formatCurrency(item.goldNet)}</td>
<td>{numberFormatter.formatCurrency(item.caliumNet)}</td>
<td>{numberFormatter.formatCurrency(item.beamNet)}</td>
<td>{numberFormatter.formatCurrency(item.rubyNet)}</td>
<td>{item.totalCurrencies}</td>
<td>
<Button theme="line" text="상세보기"

View File

@@ -60,6 +60,7 @@ const resources = {
NICKNAME_CHANGES_CONFIRM: '닉네임을 변경하시겠습니까?',
NICKNAME_CHANGES_COMPLETE: '닉네임 변경이 완료되었습니다.',
QUEST_TASK_COMPLETE_CONFIRM: '퀘스트를 강제 완료 처리하시겠습니까?',
QUEST_TASK_COMPLETE: '퀘스트를 강제 완료 요청처리가 되었습니다.\n재 조회후 상태를 확인해주세요.\n(최대 3분정도 소요)',
//table
TABLE_ITEM_DELETE_TITLE: "선택 삭제",
TABLE_BUTTON_DETAIL_TITLE: "상세보기",