feat,refac: 경매장 재설계, 유니크 경매장 구현 (#221)

- 경매장을 모든 입찰 기록이 남는 새로운 경매장 로직으로 변경
  - ng_auction, ng_auction_bid
    - DTO 사용
  - 상회입찰 시 개인메시지로 알림
- 기존의 '배경에서 조용히 이루어지는' 유니크 입찰을 공개된 유니크 경매장으로 변경
  - 종료 기간 명시
    - 종료 기간에 가까워질때 입찰하면 자동 연장
    - 최대 연장기간 있음
  - 유니크 제한인 경우 24턴 연장
  - 중원정세에서 '보물수배'로 알림

Reviewed-on: https://storage.hided.net/gitea/devsam/core/pulls/221
This commit was merged in pull request #221.
This commit is contained in:
2022-06-09 01:21:55 +09:00
parent 7964aa5f6b
commit dd38344319
64 changed files with 3053 additions and 1530 deletions
+289
View File
@@ -0,0 +1,289 @@
<template>
<div class="bg0">
<div class="bg2">거래장</div>
<div style="background-color: orange"> 구매</div>
<div class="row gx-0">
<div class="col">번호</div>
<div class="col">판매자</div>
<div class="col">물품</div>
<div class="col">수량</div>
<div class="col">시작 구매가</div>
<div class="col">현재 구매가</div>
<div class="col">즉시 구매가</div>
<div class="col">단가</div>
<div class="col">구매 예정자</div>
<div class="col">거래 종료</div>
</div>
<div v-for="auction of buyRice" :key="auction.id" class="row gx-0" @click="selectedBuyRiceAuction = auction">
<div class="col">{{ auction.id }}</div>
<div class="col">{{ auction.hostName }}</div>
<div class="col"></div>
<div class="col">{{ auction.amount }}</div>
<div class="col">{{ auction.startBidAmount }}</div>
<div class="col">{{ auction.highestBid ? auction.highestBid.amount : "-" }}</div>
<div class="col">{{ auction.finishBidAmount }}</div>
<div class="col">{{ auction.highestBid ? (auction.highestBid.amount / auction.amount).toFixed(2) : "-" }}</div>
<div class="col">{{ auction.highestBid ? auction.highestBid.generalName : "-" }}</div>
<div class="col">{{ auction.closeDate }}</div>
</div>
<div v-if="selectedBuyRiceAuction !== undefined" class="row">
<div class="col">{{ selectedBuyRiceAuction.id }} {{ selectedBuyRiceAuction.amount }} 경매에</div>
<div class="col">
<NumberInputWithInfo
v-model="bidAmountBuyRiceAuction"
:int="true"
:min="selectedBuyRiceAuction.startBidAmount"
:max="selectedBuyRiceAuction.finishBidAmount"
title="금"
:step="10"
></NumberInputWithInfo>
</div>
<div class="col"><BButton @click="bidBuyRiceAuction">입찰</BButton></div>
</div>
<div style="background-color: skyblue"> 판매</div>
<div class="row gx-0">
<div class="col">번호</div>
<div class="col">판매자</div>
<div class="col">물품</div>
<div class="col">수량</div>
<div class="col">시작 구매가</div>
<div class="col">현재 구매가</div>
<div class="col">즉시 구매가</div>
<div class="col">단가</div>
<div class="col">구매 예정자</div>
<div class="col">거래 종료</div>
</div>
<div v-for="auction of sellRice" :key="auction.id" class="row gx-0" @click="selectedSellRiceAuction = auction">
<div class="col">{{ auction.id }}</div>
<div class="col">{{ auction.hostName }}</div>
<div class="col"></div>
<div class="col">{{ auction.amount }}</div>
<div class="col">{{ auction.startBidAmount }}</div>
<div class="col">{{ auction.highestBid ? auction.highestBid.amount : "-" }}</div>
<div class="col">{{ auction.finishBidAmount }}</div>
<div class="col">{{ auction.highestBid ? (auction.highestBid.amount / auction.amount).toFixed(2) : "-" }}</div>
<div class="col">{{ auction.highestBid ? auction.highestBid.generalName : "-" }}</div>
<div class="col">{{ auction.closeDate }}</div>
</div>
<div v-if="selectedSellRiceAuction !== undefined" class="row">
<div class="col">{{ selectedSellRiceAuction.id }} {{ selectedSellRiceAuction.amount }} 경매에</div>
<div class="col">
<NumberInputWithInfo
v-model="bidAmountSellRiceAuction"
:int="true"
:min="selectedSellRiceAuction.startBidAmount"
:max="selectedSellRiceAuction.finishBidAmount"
title="쌀"
:step="10"
></NumberInputWithInfo>
</div>
<div class="col"><BButton @click="bidSellRiceAuction">입찰</BButton></div>
</div>
<div>경매 등록</div>
<div class="row">
<div class="col">
매물
<BButtonGroup>
<BButton :pressed="openAuctionInfo.type == 'buyRice'" @click="openAuctionInfo.type = 'buyRice'"> </BButton>
<BButton :pressed="openAuctionInfo.type == 'sellRice'" @click="openAuctionInfo.type = 'sellRice'">
</BButton>
</BButtonGroup>
</div>
<div class="col">
<NumberInputWithInfo
v-model="openAuctionInfo.closeTurnCnt"
:int="true"
:min="3"
:max="24"
title="기간(턴)"
:step="1"
></NumberInputWithInfo>
</div>
<div class="col">
<NumberInputWithInfo
v-model="openAuctionInfo.amount"
:int="true"
:min="100"
:max="10000"
title="수량"
:step="10"
></NumberInputWithInfo>
</div>
<div class="col">
<NumberInputWithInfo
v-model="openAuctionInfo.startBidAmount"
:int="true"
:min="100"
:max="10000"
title="시작구매가"
:step="10"
></NumberInputWithInfo>
</div>
<div class="col">
<NumberInputWithInfo
v-model="openAuctionInfo.finishBidAmount"
:int="true"
:min="100"
:max="10000"
title="즉시구매가"
:step="10"
></NumberInputWithInfo>
</div>
<div class="col">
<BButton @click="openAuction">등록</BButton>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import type { BasicResourceAuctionInfo } from "@/defs/API/Auction";
import { SammoAPI } from "@/SammoAPI";
import { unwrap } from "@/util/unwrap";
import { useToast, BButtonGroup, BButton } from "bootstrap-vue-3";
import { isString } from "lodash";
import { onMounted, reactive, ref, watch } from "vue";
import NumberInputWithInfo from "@/components/NumberInputWithInfo.vue";
const toasts = unwrap(useToast());
const buyRice = ref<BasicResourceAuctionInfo[]>([]);
const sellRice = ref<BasicResourceAuctionInfo[]>([]);
const selectedBuyRiceAuction = ref<BasicResourceAuctionInfo | undefined>(undefined);
const bidAmountBuyRiceAuction = ref<number>(0);
watch(selectedBuyRiceAuction, (auction) => {
if (!auction) {
return;
}
bidAmountBuyRiceAuction.value = auction.highestBid ? auction.highestBid.amount : auction.startBidAmount;
});
async function bidBuyRiceAuction() {
if (selectedBuyRiceAuction.value === undefined) {
return;
}
try {
await SammoAPI.Auction.BidBuyRiceAuction({
auctionID: selectedBuyRiceAuction.value.id,
amount: bidAmountBuyRiceAuction.value,
});
toasts.success({
title: "입찰 완료",
body: `입찰했습니다.`,
});
await refresh();
} catch (e) {
console.error(e);
if (isString(e)) {
toasts.danger({
title: "에러",
body: e,
});
}
}
}
const selectedSellRiceAuction = ref<BasicResourceAuctionInfo | undefined>(undefined);
const bidAmountSellRiceAuction = ref<number>(0);
watch(selectedSellRiceAuction, (auction) => {
if (!auction) {
return;
}
bidAmountSellRiceAuction.value = auction.highestBid ? auction.highestBid.amount : auction.startBidAmount;
});
async function bidSellRiceAuction() {
if (selectedSellRiceAuction.value === undefined) {
return;
}
try {
await SammoAPI.Auction.BidSellRiceAuction({
auctionID: selectedSellRiceAuction.value.id,
amount: bidAmountSellRiceAuction.value,
});
toasts.success({
title: "입찰 완료",
body: `입찰했습니다.`,
});
await refresh();
} catch (e) {
console.error(e);
if (isString(e)) {
toasts.danger({
title: "에러",
body: e,
});
}
}
}
type openAuctionT = {
type: "buyRice" | "sellRice";
amount: number;
startBidAmount: number;
finishBidAmount: number;
closeTurnCnt: number;
};
const openAuctionInfo = reactive<openAuctionT>({
type: "buyRice",
amount: 1000,
startBidAmount: 500,
finishBidAmount: 2000,
closeTurnCnt: 24,
});
async function refresh() {
try {
const result = await SammoAPI.Auction.GetActiveResourceAuctionList();
buyRice.value = result.buyRice;
sellRice.value = result.sellRice;
} catch (e) {
console.error(e);
if (isString(e)) {
toasts.danger({
title: "에러",
body: e,
});
}
}
}
async function openAuction() {
const { type, amount, startBidAmount, finishBidAmount, closeTurnCnt } = openAuctionInfo;
try {
const apiCall = type === "buyRice" ? SammoAPI.Auction.OpenBuyRiceAuction : SammoAPI.Auction.OpenSellRiceAuction;
const result = await apiCall({
amount,
startBidAmount,
finishBidAmount,
closeTurnCnt,
});
toasts.success({
title: "성공",
body: `${result.auctionID}번 경매로 등록되었습니다.`,
});
await refresh();
} catch (e) {
console.error(e);
if (isString(e)) {
toasts.danger({
title: "에러",
body: e,
});
}
}
}
onMounted(async () => {
void refresh();
console.log("mounted");
});
</script>
+155
View File
@@ -0,0 +1,155 @@
<template>
<div class="bg0">
<div class="bg2">유니크 경매장</div>
<div> 가명: {{ obfuscatedName }}</div>
<template v-if="currentAuction !== undefined">
<div class="bg1">상세</div>
<div class="row gx-0">
<div class="col-4 col-md-2 bg2">경매 번호</div>
<div class="col-4 col-md-2">{{ currentAuction.auction.id }}</div>
<div class="col-4 col-md-2 bg2">경매명</div>
<div class="col-4 col-md-2">{{ currentAuction.auction.title }}</div>
<div class="col-4 col-md-2 bg2">주최자(익명)</div>
<div class="col-4 col-md-2">{{ currentAuction.auction.hostName }}</div>
<div class="col-4 col-md-2 bg2">종료시점</div>
<div class="col-4 col-md-2">{{ currentAuction.auction.closeDate }}</div>
<div class="col-4 col-md-2 bg2">최대지연</div>
<div class="col-4 col-md-2">{{ currentAuction.auction.availableLatestBidCloseDate }}</div>
</div>
<div class="bg1">입찰자 목록</div>
<div class="row gx-0 bg2">
<div class="col-4">입찰자</div>
<div class="col-4">입찰포인트</div>
<div class="col-4">시각</div>
</div>
<div v-for="bidder of currentAuction.bidList" :key="bidder.amount" class="row gx-0">
<div class="col-4">{{ bidder.generalName }}</div>
<div class="col-4">{{ bidder.amount }}</div>
<div class="col-4">{{ bidder.date }}</div>
</div>
<div class="bg1">입찰하기</div>
<div class="row">
<div class="col">
<NumberInputWithInfo
v-model="bidAmount"
:int="true"
:min="currentAuction.bidList[0].amount"
title="유산포인트"
:step="1"
></NumberInputWithInfo>
</div>
<div class="col"><BButton @click="bidAuction">입찰</BButton></div>
</div>
</template>
<div class="bg1">목록</div>
<div v-for="[auctionID, auction] of auctionList" :key="auctionID" class="row" @click="currentAuctionID = auctionID">
<div class="col">{{ auction.id }}</div>
<div class="col">{{ auction.finished ? "종료됨" : "진행중" }}</div>
<div class="col">{{ auction.title }}</div>
<div class="col">{{ auction.hostName }}</div>
<div class="col">{{ auction.closeDate }}</div>
<div class="col">{{ auction.remainCloseDateExtensionCnt > 0 ? "남음" : "소진" }}</div>
<div class="col">{{ auction.highestBid.generalName }}</div>
<div class="col">{{ auction.highestBid.amount }}</div>
</div>
</div>
</template>
<script lang="ts" setup>
import type { UniqueItemAuctionDetail, UniqueItemAuctionList } from "@/defs/API/Auction";
import { SammoAPI } from "@/SammoAPI";
import { unwrap } from "@/util/unwrap";
import { useToast, BButton } from "bootstrap-vue-3";
import { isString } from "lodash";
import { onMounted, ref, watch } from "vue";
import NumberInputWithInfo from "@/components/NumberInputWithInfo.vue";
type AuctionItemInfo = UniqueItemAuctionList["list"][0];
const currentAuctionID = ref<number>();
const currentAuction = ref<UniqueItemAuctionDetail | undefined>(undefined);
const bidAmount = ref<number>(5000);
async function refreshDetail() {
if (currentAuctionID.value === undefined) {
return;
}
const auctionID = currentAuctionID.value;
try {
currentAuction.value = await SammoAPI.Auction.GetUniqueItemAuctionDetail({ auctionID });
} catch (e) {
console.error(e);
if (isString(e)) {
unwrap(useToast()).danger({
title: "에러",
body: e,
});
}
}
}
watch(currentAuctionID, () => {
void refreshDetail();
});
const auctionList = ref(new Map<number, AuctionItemInfo>());
const obfuscatedName = ref("");
const toasts = unwrap(useToast());
async function refresh() {
try {
const result = await SammoAPI.Auction.GetUniqueItemAuctionList();
obfuscatedName.value = result.obfuscatedName;
auctionList.value = new Map(result.list.map((auction) => [auction.id, auction]));
if (currentAuctionID.value === undefined && result.list.length > 0) {
currentAuctionID.value = result.list[0].id;
bidAmount.value = result.list[0].highestBid.amount;
}
} catch (e) {
console.error(e);
if (isString(e)) {
toasts.danger({
title: "에러",
body: e,
});
}
}
}
async function bidAuction() {
if (currentAuction.value === undefined) {
return;
}
const amount = bidAmount.value;
const auctionInfo = currentAuction.value.auction;
if (confirm(`${auctionInfo.title}${amount}유산포인트를 입찰하시겠습니까?`)) {
try {
await SammoAPI.Auction.BidUniqueAuction({ auctionID: auctionInfo.id, amount });
toasts.success({
title: "성공",
body: "입찰이 완료되었습니다.",
});
void refreshDetail();
void refresh();
} catch (e) {
console.error(e);
if (isString(e)) {
toasts.danger({
title: "에러",
body: e,
});
}
}
}
}
onMounted(() => {
void refresh();
});
</script>