refac: linter 관련 설정 변경 및 적용, map_theme 변수 제거

- eslint에 prettier 조합
- prettierrc에 width 120, tabWidth 2
- gameStor->map_theme 제거
- map_theme, mapTheme를 GameConst::$mapName으로 대체
- eslint에서 vue/vue3-essential 대신 vue3-recommended 적용
  - vue/max-attributes-per-line 완화
  - vue/v-on-event-hyphenation 해제
  - vue/attribute-hyphenation 해제
- 일부 tsc import type warning 해결
- 일부 vue template type warning 해결
- 일부 vue SFC를 script setup으로 변경
  - TipTap
  - TopBackBar
  - BottomBackBar
  - BoardArticle
  - ProcessCity
This commit is contained in:
2022-03-29 02:06:47 +09:00
parent e3820cdd8a
commit f7ce963fb7
79 changed files with 4541 additions and 3020 deletions
+383 -382
View File
@@ -1,150 +1,159 @@
<template>
<div v-if="bettingDetailInfo !== undefined && info !== undefined">
<div class="bg2">
{{ info.name }}
<span v-if="info.finished">(종료)</span>
<span
v-else-if="(yearMonth ?? 0) <= info.closeYearMonth"
>({{ parseYearMonth(info.closeYearMonth)[0] }} {{ parseYearMonth(info.closeYearMonth)[1] }}월까지)</span>
<span v-else>(베팅 마감)</span>
(총액: {{ bettingAmount.toLocaleString() }})
</div>
<div class="row bettingCandidates gx-1 gy-1">
<div
class="col-4 col-md-2"
v-for="(candidate, idx) in info.candidates"
:key="idx"
@click="toggleCandidate(idx)"
>
<div
:class="[
'bettingCandidate',
pickedBetType.has(idx) ? 'picked' : undefined,
(info.finished && winner.has(idx)) ? 'picked' : undefined,
]"
>
<div class="title bg1">{{ candidate.title }}</div>
<div class="info" v-if="candidate.isHtml" v-html="candidate.info"></div>
<div
class="pickRate"
>선택율: {{ ((partialBet.get(idx) ?? 0) / pureBettingAmount * 100).toFixed(1) }}%</div>
</div>
</div>
</div>
<div v-if="!info.finished && (yearMonth ?? 0) <= info.closeYearMonth" class="row gx-0">
<div
class="col-6 col-md-3 align-self-center"
>잔여 {{ info.reqInheritancePoint ? '포인트' : '금' }} : {{ bettingDetailInfo.remainPoint.toLocaleString() }}</div>
<div
class="col-6 col-md-3 align-self-center"
>사용 포인트: {{ sum(Array.from(myBettings.values())).toLocaleString() }}</div>
<div class="col-6 col-md-3 align-self-center">대상: {{ getTypeStr(pickedBetTypeKey) }}</div>
<div class="col-4 col-md-2 d-grid">
<b-form-input
class="d-grid"
type="number"
v-model="betPoint"
:min="10"
:max="1000"
:step="10"
></b-form-input>
</div>
<div class="col-2 col-md-1 d-grid">
<b-button class="d-grid" @click="submitBet">베팅</b-button>
</div>
</div>
<div>
<div class="bg2">배당 순위</div>
<div
class="row"
:style="{
borderBottom: 'gray solid 1px'
}"
>
<div class="col-5 text-center">대상</div>
<div class="col-2 text-center">베팅액</div>
<div class="col-3 text-center"> 베팅</div>
<div class="col-2 text-center">{{ info.finished ? '배율' : '기대 배율' }}</div>
</div>
<template v-if="info.finished">
<div class="row" v-for="[betType, amount] of detailBet" :key="betType">
<template
v-for="[matchPoint, color] of [calcMatchPointWithColor(betType)]"
:key="matchPoint"
>
<div
class="col-5"
:style="{
fontWeight: myBettings.has(betType) ? 'bold' : undefined,
color: color
}"
>{{ getTypeStr(betType) }}</div>
<div class="col-2 text-end">{{ amount.toLocaleString() }}</div>
<div class="col-3 text-center" v-if="myBettings.has(betType)">
<template
v-for="subPoint of [myBettings.get(betType) ?? 0]"
>({{ subPoint.toLocaleString() }} -> {{ calculatedReward[matchPoint] == 0 ? 0 : (subPoint * calculatedReward[matchPoint] / (calculatedSubAmount.get(matchPoint) ?? 1)).toFixed(1).toLocaleString() }})</template>
</div>
<div class="col-3 text-center" v-else></div>
<div
class="col-2 text-end"
>{{ (calculatedReward[matchPoint] == 0 ? 0 : (calculatedReward[matchPoint] / (calculatedSubAmount.get(matchPoint) ?? 1))).toFixed(1).toLocaleString() }}</div>
</template>
</div>
</template>
<template v-else>
<div class="row" v-for="[betType, amount] of detailBet" :key="betType">
<div
class="col-5"
:style="{
fontWeight: myBettings.has(betType) ? 'bold' : undefined,
}"
>{{ getTypeStr(betType) }}</div>
<div class="col-2 text-end">{{ amount.toLocaleString() }}</div>
<div class="col-3 text-center" v-if="myBettings.has(betType)">
<template
v-for="subPoint of [myBettings.get(betType) ?? 0]"
>({{ subPoint.toLocaleString() }} -> {{ (subPoint * maxBettingReward / amount).toFixed(1).toLocaleString() }})</template>
</div>
<div class="col-3 text-center" v-else></div>
<div
class="col-2 text-end"
>{{ (maxBettingReward / amount).toFixed(1).toLocaleString() }}</div>
</div>
</template>
</div>
<div v-if="bettingDetailInfo !== undefined && info !== undefined">
<div class="bg2">
{{ info.name }}
<span v-if="info.finished">(종료)</span>
<span v-else-if="(yearMonth ?? 0) <= info.closeYearMonth"
>({{ parseYearMonth(info.closeYearMonth)[0] }} {{ parseYearMonth(info.closeYearMonth)[1] }}월까지)</span
>
<span v-else>(베팅 마감)</span>
(총액: {{ bettingAmount.toLocaleString() }})
</div>
<div class="row bettingCandidates gx-1 gy-1">
<div v-for="(candidate, idx) in info.candidates" :key="idx" class="col-4 col-md-2" @click="toggleCandidate(idx)">
<div
:class="[
'bettingCandidate',
pickedBetType.has(idx) ? 'picked' : undefined,
info.finished && winner.has(idx) ? 'picked' : undefined,
]"
>
<div class="title bg1">
{{ candidate.title }}
</div>
<!-- eslint-disable-next-line vue/no-v-html -->
<div v-if="candidate.isHtml" class="info" v-html="candidate.info" />
<div class="pickRate">선택율: {{ (((partialBet.get(idx) ?? 0) / pureBettingAmount) * 100).toFixed(1) }}%</div>
</div>
</div>
</div>
<div v-if="!info.finished && (yearMonth ?? 0) <= info.closeYearMonth" class="row gx-0">
<div class="col-6 col-md-3 align-self-center">
잔여 {{ info.reqInheritancePoint ? "포인트" : "금" }} : {{ bettingDetailInfo.remainPoint.toLocaleString() }}
</div>
<div class="col-6 col-md-3 align-self-center">
사용 포인트: {{ sum(Array.from(myBettings.values())).toLocaleString() }}
</div>
<div class="col-6 col-md-3 align-self-center">대상: {{ getTypeStr(pickedBetTypeKey) }}</div>
<div class="col-4 col-md-2 d-grid">
<!-- eslint-disable-next-line vue/max-attributes-per-line -->
<b-form-input v-model="betPoint" class="d-grid" type="number" :min="10" :max="1000" :step="10" />
</div>
<div class="col-2 col-md-1 d-grid">
<b-button class="d-grid" @click="submitBet"> 베팅 </b-button>
</div>
</div>
<div>
<div class="bg2">배당 순위</div>
<div
class="row"
:style="{
borderBottom: 'gray solid 1px',
}"
>
<div class="col-5 text-center">대상</div>
<div class="col-2 text-center">베팅액</div>
<div class="col-3 text-center"> 베팅</div>
<div class="col-2 text-center">
{{ info.finished ? "배율" : "기대 배율" }}
</div>
</div>
<template v-if="info.finished">
<div v-for="[betType, amount] of detailBet" :key="betType" class="row">
<template v-for="[matchPoint, color] of [calcMatchPointWithColor(betType)]" :key="matchPoint">
<div
class="col-5"
:style="{
fontWeight: myBettings.has(betType) ? 'bold' : undefined,
color: color,
}"
>
{{ getTypeStr(betType) }}
</div>
<div class="col-2 text-end">
{{ amount.toLocaleString() }}
</div>
<div v-if="myBettings.has(betType)" class="col-3 text-center">
<template v-for="subPoint of [myBettings.get(betType) ?? 0]">
({{ subPoint.toLocaleString() }} ->
{{
calculatedReward[matchPoint] == 0
? 0
: ((subPoint * calculatedReward[matchPoint]) / (calculatedSubAmount.get(matchPoint) ?? 1))
.toFixed(1)
.toLocaleString()
}})
</template>
</div>
<div v-else class="col-3 text-center" />
<div class="col-2 text-end">
{{
(calculatedReward[matchPoint] == 0
? 0
: calculatedReward[matchPoint] / (calculatedSubAmount.get(matchPoint) ?? 1)
)
.toFixed(1)
.toLocaleString()
}}
</div>
</template>
</div>
</template>
<template v-else>
<div v-for="[betType, amount] of detailBet" :key="betType" class="row">
<div
class="col-5"
:style="{
fontWeight: myBettings.has(betType) ? 'bold' : undefined,
}"
>
{{ getTypeStr(betType) }}
</div>
<div class="col-2 text-end">
{{ amount.toLocaleString() }}
</div>
<div v-if="myBettings.has(betType)" class="col-3 text-center">
<template v-for="subPoint of [myBettings.get(betType) ?? 0]">
({{ subPoint.toLocaleString() }} ->
{{ ((subPoint * maxBettingReward) / amount).toFixed(1).toLocaleString() }})
</template>
</div>
<div v-else class="col-3 text-center" />
<div class="col-2 text-end">{{ (maxBettingReward / amount).toFixed(1).toLocaleString() }}</div>
</div>
</template>
</div>
</div>
</template>
<script setup lang="ts">
import type { BettingInfo, ToastType } from '@/defs';
import type { BettingInfo, ToastType } from "@/defs";
import { SammoAPI, type ValidResponse } from "@/SammoAPI";
import { joinYearMonth } from '@/util/joinYearMonth';
import { parseYearMonth } from '@/util/parseYearMonth';
import { isString, range, sum } from 'lodash';
import { joinYearMonth } from "@/util/joinYearMonth";
import { parseYearMonth } from "@/util/parseYearMonth";
import { isString, range, sum } from "lodash";
import { ref, type PropType, watch } from "vue";
type BettingDetailResponse = ValidResponse & {
bettingInfo: BettingInfo;
bettingDetail: [string, number][];
myBetting: [string, number][];
remainPoint: number;
year: number;
month: number;
}
bettingInfo: BettingInfo;
bettingDetail: [string, number][];
myBetting: [string, number][];
remainPoint: number;
year: number;
month: number;
};
const props = defineProps({
bettingID: {
type: Number as PropType<number>,
required: true,
}
bettingID: {
type: Number as PropType<number>,
required: true,
},
});
const emit = defineEmits<{
(event: 'reqToast', content: ToastType): void,
(event: "reqToast", content: ToastType): void;
}>();
const year = ref<number>(0);
@@ -163,24 +172,26 @@ const detailBet = ref<[string, number][]>([]);
const typeMap = ref(new Map<string, string>());
function getTypeStr(type: string): string {
const typeResult = typeMap.value.get(type);
if (typeResult !== undefined) {
return typeResult;
}
const bettingSubTypes = JSON.parse(type) as number[];
if (bettingSubTypes[0] < -1) {
return 'Invalid';
}
const typeResult = typeMap.value.get(type);
if (typeResult !== undefined) {
return typeResult;
}
const bettingSubTypes = JSON.parse(type) as number[];
if (bettingSubTypes[0] < -1) {
return "Invalid";
}
const textBettingType = bettingSubTypes.map((idx) => {
return bettingDetailInfo.value?.bettingInfo.candidates[idx].title;
}).join(', ');
typeMap.value.set(type, textBettingType);
return textBettingType;
const textBettingType = bettingSubTypes
.map((idx) => {
return bettingDetailInfo.value?.bettingInfo.candidates[idx].title;
})
.join(", ");
typeMap.value.set(type, textBettingType);
return textBettingType;
}
const pickedBetType = ref(new Set<number>());
const pickedBetTypeKey = ref('[]');
const pickedBetTypeKey = ref("[]");
const betPoint = ref(0);
const myBettings = ref(new Map<string, number>());
@@ -189,274 +200,264 @@ const winner = ref(new Set<number>());
const calculatedReward = ref<number[]>([]);
const calculatedSubAmount = ref(new Map<number, number>());
function calcMatchPointWithColor(type: string): [number, "green" | "yellow" | "red" | undefined] {
if (!info.value?.finished) {
return [0, undefined];
}
const bettingSubTypes = JSON.parse(type) as number[];
if (bettingSubTypes[0] < -1) {
return [0, undefined];
}
function calcMatchPointWithColor(type: string): [number, 'green' | 'yellow' | 'red' | undefined] {
if (!info.value?.finished) {
return [0, undefined];
}
const bettingSubTypes = JSON.parse(type) as number[];
if (bettingSubTypes[0] < -1) {
return [0, undefined];
let matchPoint = 0;
for (const subType of bettingSubTypes) {
if (winner.value.has(subType)) {
matchPoint += 1;
}
}
let matchPoint = 0;
for (const subType of bettingSubTypes) {
if (winner.value.has(subType)) {
matchPoint += 1;
}
if (info.value.isExclusive) {
if (matchPoint == info.value.selectCnt) {
return [matchPoint, "green"];
} else {
return [matchPoint, "red"];
}
}
if (info.value.isExclusive) {
if (matchPoint == info.value.selectCnt) {
return [matchPoint, 'green'];
}
else {
return [matchPoint, 'red'];
}
}
let color: 'green' | 'red' | 'yellow' = 'green';
if (matchPoint == 0) {
color = 'red';
}
else if (matchPoint < info.value.selectCnt) {
color = 'yellow';
}
return [matchPoint, color];
let color: "green" | "red" | "yellow" = "green";
if (matchPoint == 0) {
color = "red";
} else if (matchPoint < info.value.selectCnt) {
color = "yellow";
}
return [matchPoint, color];
}
function calcReward() {
if (info.value === undefined || bettingDetailInfo.value === undefined) {
throw 'no info';
}
const selectCnt = info.value.selectCnt;
const rewardAmount = new Array<number>(selectCnt).fill(0);
const subAmount = new Map<number, number>();
for (const [bettingTypeStr, amount] of bettingDetailInfo.value.bettingDetail) {
if (amount == 0) {
continue;
}
const [matchPoint,] = calcMatchPointWithColor(bettingTypeStr);
subAmount.set(matchPoint, (subAmount.get(matchPoint) ?? 0) + amount);
}
calculatedSubAmount.value = subAmount;
if (selectCnt == 1){
rewardAmount[selectCnt - 1] = bettingAmount.value;
calculatedReward.value = rewardAmount;
return;
}
if (info.value.isExclusive) {
rewardAmount[selectCnt - 1] = bettingAmount.value;
calculatedReward.value = rewardAmount;
return;
}
let remainRewardAmount = bettingAmount.value;
for (const matchPoint of range(selectCnt, 0, -1)) {
if (!subAmount.has(matchPoint)) {
continue;
}
const givenRewardAmount = remainRewardAmount / 2;
rewardAmount[matchPoint] = givenRewardAmount;
remainRewardAmount -= givenRewardAmount; // /2가 아니라 다른 값이 될 경우를 대비..
}
for (const matchPoint of range(1, selectCnt + 1)) {
if (!subAmount.has(matchPoint)) {
continue;
}
rewardAmount[matchPoint] += remainRewardAmount;
break;
if (info.value === undefined || bettingDetailInfo.value === undefined) {
throw "no info";
}
const selectCnt = info.value.selectCnt;
const rewardAmount = new Array<number>(selectCnt).fill(0);
const subAmount = new Map<number, number>();
for (const [bettingTypeStr, amount] of bettingDetailInfo.value.bettingDetail) {
if (amount == 0) {
continue;
}
const [matchPoint] = calcMatchPointWithColor(bettingTypeStr);
subAmount.set(matchPoint, (subAmount.get(matchPoint) ?? 0) + amount);
}
calculatedSubAmount.value = subAmount;
if (selectCnt == 1) {
rewardAmount[selectCnt - 1] = bettingAmount.value;
calculatedReward.value = rewardAmount;
return;
}
if (info.value.isExclusive) {
rewardAmount[selectCnt - 1] = bettingAmount.value;
calculatedReward.value = rewardAmount;
return;
}
let remainRewardAmount = bettingAmount.value;
for (const matchPoint of range(selectCnt, 0, -1)) {
if (!subAmount.has(matchPoint)) {
continue;
}
const givenRewardAmount = remainRewardAmount / 2;
rewardAmount[matchPoint] = givenRewardAmount;
remainRewardAmount -= givenRewardAmount; // /2가 아니라 다른 값이 될 경우를 대비..
}
for (const matchPoint of range(1, selectCnt + 1)) {
if (!subAmount.has(matchPoint)) {
continue;
}
rewardAmount[matchPoint] += remainRewardAmount;
break;
}
calculatedReward.value = rewardAmount;
}
async function loadBetting(bettingID: number) {
try {
const result = await SammoAPI.Betting.GetBettingDetail<BettingDetailResponse>({
betting_id: bettingID
});
year.value = result.year;
month.value = result.month;
yearMonth.value = joinYearMonth(result.year, result.month);
bettingDetailInfo.value = result;
info.value = result.bettingInfo;
try {
const result = await SammoAPI.Betting.GetBettingDetail<BettingDetailResponse>({
betting_id: bettingID,
});
year.value = result.year;
month.value = result.month;
yearMonth.value = joinYearMonth(result.year, result.month);
bettingDetailInfo.value = result;
info.value = result.bettingInfo;
partialBet.value.clear();
partialBet.value.clear();
const betSort = new Map<string, number>();
const betSort = new Map<string, number>();
let _bettingAmount = 0;
let adminBettingAmount = 0;
for (const [bettingType, amount] of result.bettingDetail) {
console.log(amount, typeof (amount));
let userBet = true;
const bettingSubTypes = JSON.parse(bettingType) as number[];
for (const bettingSubType of bettingSubTypes) {
if (bettingSubType < 0) {
userBet = false;
continue;
}
const oldValue = partialBet.value.get(bettingSubType) ?? 0;
partialBet.value.set(bettingSubType, oldValue + amount);
}
if (userBet) {
const oldValue = betSort.get(bettingType) ?? 0;
betSort.set(bettingType, oldValue + amount);
}
_bettingAmount += amount;
if (!userBet) {
adminBettingAmount += amount;
}
}
console.log(_bettingAmount);
bettingAmount.value = _bettingAmount;
pureBettingAmount.value = _bettingAmount - adminBettingAmount;
if (info.value.isExclusive || info.value.selectCnt == 1) {
maxBettingReward.value = _bettingAmount;
} else {
maxBettingReward.value = _bettingAmount / 2;
let _bettingAmount = 0;
let adminBettingAmount = 0;
for (const [bettingType, amount] of result.bettingDetail) {
console.log(amount, typeof amount);
let userBet = true;
const bettingSubTypes = JSON.parse(bettingType) as number[];
for (const bettingSubType of bettingSubTypes) {
if (bettingSubType < 0) {
userBet = false;
continue;
}
const oldValue = partialBet.value.get(bettingSubType) ?? 0;
partialBet.value.set(bettingSubType, oldValue + amount);
}
detailBet.value = Array.from(betSort.entries());
detailBet.value.sort(([, lhsVal], [, rhsVal]) => {
return rhsVal - lhsVal;
})
if (userBet) {
const oldValue = betSort.get(bettingType) ?? 0;
betSort.set(bettingType, oldValue + amount);
}
pickedBetType.value.clear();
pickedBetTypeKey.value = '[]';
myBettings.value.clear();
if (result.bettingInfo.winner) {
winner.value = new Set(result.bettingInfo.winner);
}
else {
winner.value.clear();
}
for (const [betType, amount] of result.myBetting) {
myBettings.value.set(betType, amount);
}
calcReward();
} catch (e) {
if (isString(e)) {
emit('reqToast', {
content: {
title: "에러",
body: e
},
options: {
variant: 'danger',
}
});
}
console.error(e);
_bettingAmount += amount;
if (!userBet) {
adminBettingAmount += amount;
}
}
console.log(_bettingAmount);
bettingAmount.value = _bettingAmount;
pureBettingAmount.value = _bettingAmount - adminBettingAmount;
if (info.value.isExclusive || info.value.selectCnt == 1) {
maxBettingReward.value = _bettingAmount;
} else {
maxBettingReward.value = _bettingAmount / 2;
}
detailBet.value = Array.from(betSort.entries());
detailBet.value.sort(([, lhsVal], [, rhsVal]) => {
return rhsVal - lhsVal;
});
pickedBetType.value.clear();
pickedBetTypeKey.value = "[]";
myBettings.value.clear();
if (result.bettingInfo.winner) {
winner.value = new Set(result.bettingInfo.winner);
} else {
winner.value.clear();
}
for (const [betType, amount] of result.myBetting) {
myBettings.value.set(betType, amount);
}
calcReward();
} catch (e) {
if (isString(e)) {
emit("reqToast", {
content: {
title: "에러",
body: e,
},
options: {
variant: "danger",
},
});
}
console.error(e);
}
}
void loadBetting(props.bettingID);
watch(() => props.bettingID, (newBettingID) => {
watch(
() => props.bettingID,
(newBettingID) => {
void loadBetting(newBettingID);
});
}
);
function toggleCandidate(idx: number) {
if (info.value === undefined) {
return;
}
if (bettingDetailInfo.value === undefined) {
return;
}
if (info.value.closeYearMonth < yearMonth.value) {
return;
}
if (info.value.finished) {
return;
}
const selectCnt = bettingDetailInfo.value.bettingInfo.selectCnt;
if (info.value === undefined) {
return;
}
if (bettingDetailInfo.value === undefined) {
return;
}
if (info.value.closeYearMonth < yearMonth.value) {
return;
}
if (info.value.finished) {
return;
}
const selectCnt = bettingDetailInfo.value.bettingInfo.selectCnt;
if (selectCnt == 1) {
pickedBetType.value.clear();
pickedBetType.value.add(idx);
pickedBetTypeKey.value = JSON.stringify([idx]);
return;
}
if (selectCnt == 1) {
pickedBetType.value.clear();
pickedBetType.value.add(idx);
pickedBetTypeKey.value = JSON.stringify([idx]);
return;
}
if (pickedBetType.value.has(idx)) {
pickedBetType.value.delete(idx);
}
else if (pickedBetType.value.size < selectCnt) {
pickedBetType.value.add(idx);
}
else {
emit('reqToast', {
content: {
title: '오류',
body: `이미 ${selectCnt}개를 선택했습니다.`,
},
options: {
variant: 'warning',
}
});
return;
}
if (pickedBetType.value.has(idx)) {
pickedBetType.value.delete(idx);
} else if (pickedBetType.value.size < selectCnt) {
pickedBetType.value.add(idx);
} else {
emit("reqToast", {
content: {
title: "오류",
body: `이미 ${selectCnt}개를 선택했습니다.`,
},
options: {
variant: "warning",
},
});
return;
}
const typeArr = Array.from(pickedBetType.value.values());
pickedBetTypeKey.value = JSON.stringify(typeArr.sort((lhs, rhs) => lhs - rhs));
const typeArr = Array.from(pickedBetType.value.values());
pickedBetTypeKey.value = JSON.stringify(typeArr.sort((lhs, rhs) => lhs - rhs));
}
async function submitBet(): Promise<void> {
const bettingInfo = info.value;
if (bettingInfo === undefined) {
return;
}
const bettingInfo = info.value;
if (bettingInfo === undefined) {
return;
const bettingID = bettingInfo.id;
const bettingType = JSON.parse(pickedBetTypeKey.value);
const amount = betPoint.value;
try {
await SammoAPI.Betting.Bet({
bettingID,
bettingType,
amount,
});
emit("reqToast", {
content: {
title: "완료",
body: "베팅했습니다",
},
options: {
variant: "success",
},
});
await loadBetting(bettingInfo.id);
} catch (e) {
if (isString(e)) {
emit("reqToast", {
content: {
title: "에러",
body: e,
},
options: {
variant: "danger",
},
});
}
const bettingID = bettingInfo.id;
const bettingType = JSON.parse(pickedBetTypeKey.value);
const amount = betPoint.value;
try {
await SammoAPI.Betting.Bet({
bettingID,
bettingType,
amount,
});
emit('reqToast', {
content: {
title: '완료',
body: '베팅했습니다',
},
options: {
variant: 'success'
}
});
await loadBetting(bettingInfo.id);
} catch (e) {
if (isString(e)) {
emit('reqToast', {
content: {
title: "에러",
body: e,
},
options: {
variant: "danger",
}
});
}
console.error(e);
}
console.error(e);
}
}
</script>
</script>
+62 -71
View File
@@ -1,27 +1,26 @@
<template>
<div class="articleFrame bg0">
<div class="bg1 row gx-0">
<div class="authorName center">{{ article.author }}</div>
<div class="col articleTitle center">{{ article.title }}</div>
<div class="col-2 col-md-1 date center">{{ article.date.slice(5, 16) }}</div>
<div class="authorName center">
{{ article.author }}
</div>
<div class="col articleTitle center">
{{ article.title }}
</div>
<div class="col-2 col-md-1 date center">
{{ article.date.slice(5, 16) }}
</div>
</div>
<div class="row gx-0 s-border-b">
<div class="col-2 col-md-1 authorIcon center">
<img
class="generalIcon"
width="64"
height="64"
:src="article.author_icon"
/>
<img class="generalIcon" width="64" height="64" :src="article.author_icon" />
</div>
<div class="col text">
{{ article.text }}
</div>
<div class="col text">{{ article.text }}</div>
</div>
<div class="commentList">
<board-comment
v-for="comment in article.comment"
:key="comment.no"
:comment="comment"
/>
<board-comment v-for="comment in article.comment" :key="comment.no" :comment="comment" />
</div>
<div class="row gx-0">
<div class="bg2 inputCommentHeader center d-grid">
@@ -29,86 +28,78 @@
</div>
<div class="col d-grid">
<input
v-model.trim="newCommentText"
class="commentText"
type="text"
maxlength="250"
placeholder="새 댓글 내용"
v-model.trim="newCommentText"
@keyup.enter="submitComment"
/>
</div>
<div class="col-2 col-md-1 d-grid">
<b-button class="submitComment" @click="submitComment" size="sm"
>등록</b-button
>
<b-button class="submitComment" size="sm" @click="submitComment"> 등록 </b-button>
</div>
</div>
</div>
</template>
<script lang="ts">
<script lang="ts" setup>
import type { BoardArticleItem } from "@/PageBoard.vue";
import BoardComment from "@/components/BoardComment.vue";
import { defineComponent, type PropType } from "vue";
import { ref, type PropType } from "vue";
import axios from "axios";
import { convertFormData } from "@util/convertFormData";
import type { InvalidResponse } from "@/defs";
export default defineComponent({
name: "BoardArticle",
components: {
BoardComment,
},
data() {
return {
newCommentText: "",
};
},
props: {
article: {
type: Object as PropType<BoardArticleItem>,
required: true,
},
},
emits: ["submit-comment"],
methods: {
async submitComment() {
const comment = this.newCommentText;
if (!comment) {
return;
}
const articleNo = this.article.no;
let result: InvalidResponse;
const newCommentText = ref("");
try {
const response = await axios({
url: "j_board_comment_add.php",
method: "post",
responseType: "json",
data: convertFormData({
articleNo: articleNo,
text: comment,
}),
});
result = response.data;
if (!result.result) {
throw result.reason;
}
} catch (e) {
console.error(e);
alert(`실패했습니다: ${e}`);
return;
}
this.newCommentText = "";
this.$emit("submit-comment");
},
const props = defineProps({
article: {
type: Object as PropType<BoardArticleItem>,
required: true,
},
});
const emit = defineEmits<{
(event: "submit-comment"): void;
}>();
async function submitComment() {
const comment = newCommentText.value;
if (!comment) {
return;
}
const articleNo = props.article.no;
let result: InvalidResponse;
try {
const response = await axios({
url: "j_board_comment_add.php",
method: "post",
responseType: "json",
data: convertFormData({
articleNo: articleNo,
text: comment,
}),
});
result = response.data;
if (!result.result) {
throw result.reason;
}
} catch (e) {
console.error(e);
alert(`실패했습니다: ${e}`);
return;
}
newCommentText.value = "";
emit("submit-comment");
}
</script>
<style>
td.text {
white-space: pre;
}
</style>
</style>
+13 -3
View File
@@ -1,8 +1,18 @@
<template>
<div class="row gx-0 comment s-border-b">
<div class="authorName center d-grid"><div class="align-self-center">{{ comment.author }}</div></div>
<div class="col text">{{ comment.text }}</div>
<div class="col-2 col-md-1 date center d-grid"><div class="align-self-center">{{ comment.date.slice(5, 16) }}</div></div>
<div class="authorName center d-grid">
<div class="align-self-center">
{{ comment.author }}
</div>
</div>
<div class="col text">
{{ comment.text }}
</div>
<div class="col-2 col-md-1 date center d-grid">
<div class="align-self-center">
{{ comment.date.slice(5, 16) }}
</div>
</div>
</div>
</template>
<script lang="ts">
+21 -32
View File
@@ -1,41 +1,30 @@
<template>
<div class="bg0" style="padding-top:20px;">
<button type="button" class="btn btn-sammo-base2 back_btn" @click="back">
돌아가기
</button>
<div></div>
<div class="bg0" style="padding-top: 20px">
<button type="button" class="btn btn-sammo-base2 back_btn" @click="back">돌아가기</button>
<div />
</div>
</template>
<script lang="ts">
import { defineComponent, type PropType } from "vue";
<script lang="ts" setup>
import type { PropType } from "vue";
import "@scss/game_bg.scss";
export default defineComponent({
methods: {
back(){
if(this.type === 'normal'){
location.href = './';
}
else if(this.type == 'chief'){
location.href = 'v_chiefCenter.php';
}
else{
//TODO: window.close하려면 부모창이 있어야함!
window.close();
}
}
},
props: {
type: {
type: String as PropType<"normal"|"chief"|"close">,
default: "normal",
required: false,
},
const props = defineProps({
type: {
type: String as PropType<"normal" | "chief" | "close">,
default: "normal",
required: false,
},
});
function back() {
if (props.type === "normal") {
location.href = "./";
} else if (props.type == "chief") {
location.href = "v_chiefCenter.php";
} else {
//TODO: window.close하려면 부모창이 있어야함!
window.close();
}
}
</script>
<style>
</style>
+166 -191
View File
@@ -7,15 +7,11 @@
:style="{
color: getNpcColor(officer.npcType ?? 0),
}"
>{{ officer.name }}</div>
>
{{ officer.name }}
</div>
</div>
<div
:class="[
'row',
'controlPad',
props.targetIsMe ? 'targetIsMe' : 'targetIsNotMe',
]"
>
<div :class="['row', 'controlPad', props.targetIsMe ? 'targetIsMe' : 'targetIsNotMe']">
<div class="col-3 col-md-12 order-md-last">
<div class="d-grid mb-1 py-1 only500px bg1 center">
<div
@@ -23,51 +19,50 @@
color: getNpcColor(officer.npcType ?? 0),
fontSize: '1.2em',
}"
>{{ officer.name }}</div>
>
{{ officer.name }}
</div>
<div>{{ officer.officerLevelText }}</div>
</div>
<div class="row gx-1 gy-1 py-1">
<div class="col-md-4 mx-0 mb-0 mt-1 d-grid">
<div class="alert alert-primary mb-0 center" style="padding: 0.5rem 0"><SimpleClock :serverTime="parseTime(props.date)" /></div>
<div class="alert alert-primary mb-0 center" style="padding: 0.5rem 0">
<SimpleClock :serverTime="parseTime(props.date)" />
</div>
</div>
<div class="col-md-4 d-grid">
<BButton
variant="secondary"
@click="isEditMode = !isEditMode"
>{{ isEditMode ? '일반 모드' : '고급 모드' }}</BButton>
<BButton variant="secondary" @click="isEditMode = !isEditMode">
{{ isEditMode ? "일반 모드" : "고급 모드" }}
</BButton>
</div>
<BDropdown class="col-md-4" text="반복">
<BDropdownItem
v-for="turnIdx in maxPushTurn"
:key="turnIdx"
@click="repeatNationCommand(turnIdx)"
>
{{
turnIdx
}}
<BDropdownItem v-for="turnIdx in maxPushTurn" :key="turnIdx" @click="repeatNationCommand(turnIdx)">
{{ turnIdx }}
</BDropdownItem>
</BDropdown>
<template v-if="isEditMode">
<BDropdown class="col-md-4" left text="범위">
<BDropdownItem @click="queryActionHelper.selectTurn()">해제</BDropdownItem>
<BDropdownItem @click="queryActionHelper.selectAll()">모든턴</BDropdownItem>
<BDropdownItem @click="queryActionHelper.selectStep(0, 2)">홀수턴</BDropdownItem>
<BDropdownItem @click="queryActionHelper.selectStep(1, 2)">짝수턴</BDropdownItem>
<BDropdownDivider></BDropdownDivider>
<BDropdownItem @click="queryActionHelper.selectTurn()"> 해제 </BDropdownItem>
<BDropdownItem @click="queryActionHelper.selectAll()"> 모든턴 </BDropdownItem>
<BDropdownItem @click="queryActionHelper.selectStep(0, 2)"> 홀수턴 </BDropdownItem>
<BDropdownItem @click="queryActionHelper.selectStep(1, 2)"> 짝수턴 </BDropdownItem>
<BDropdownDivider />
<BDropdownText v-for="spanIdx in [3, 4, 5, 6, 7]" :key="spanIdx">
{{ spanIdx }} 간격
<br />
<BButtonGroup>
<BButton
class="ignoreMe"
v-for="beginIdx in spanIdx"
:key="beginIdx"
class="ignoreMe"
@click="queryActionHelper.selectStep(beginIdx - 1, spanIdx)"
>{{ beginIdx }}</BButton>
>
{{ beginIdx }}
</BButton>
</BButtonGroup>
</BDropdownText>
</BDropdown>
@@ -79,7 +74,7 @@
@click.self="useStoredAction(actions)"
>
{{ actionKey }}
<BButton @click.prevent="deleteStoredActions(actionKey)" size="sm">삭제</BButton>
<BButton size="sm" @click.prevent="deleteStoredActions(actionKey)"> 삭제 </BButton>
</BDropdownItem>
</BDropdown>
@@ -90,34 +85,20 @@
:key="idx"
@click="void reserveCommandDirect([[Array.from(selectedTurnList.values()), action]])"
>
{{
action.brief
}}
{{ action.brief }}
</BDropdownItem>
</BDropdown>
</div>
</template>
<BDropdown class="col-md-6" split text="당기기" @click="pullNationCommandSingle">
<BDropdownItem
v-for="turnIdx in maxPushTurn"
:key="turnIdx"
@click="pushNationCommand(-turnIdx)"
>
{{
turnIdx
}}
<BDropdownItem v-for="turnIdx in maxPushTurn" :key="turnIdx" @click="pushNationCommand(-turnIdx)">
{{ turnIdx }}
</BDropdownItem>
</BDropdown>
<BDropdown class="col-md-6" split text="미루기" @click="pushNationCommandSingle">
<BDropdownItem
v-for="turnIdx in maxPushTurn"
:key="turnIdx"
@click="pushNationCommand(turnIdx)"
>
{{
turnIdx
}}
<BDropdownItem v-for="turnIdx in maxPushTurn" :key="turnIdx" @click="pushNationCommand(turnIdx)">
{{ turnIdx }}
</BDropdownItem>
</BDropdown>
</div>
@@ -134,28 +115,28 @@
}"
>
<CommandSelectForm
:commandList="commandList"
ref="commandQuickReserveForm"
@on-close="chooseQuickReserveCommand($event)"
:hideClose="false"
v-model:activatedCategory="activatedCategory"
:commandList="commandList"
:hideClose="false"
class="bg-dark"
style="position:absolute"
style="position: absolute"
@onClose="chooseQuickReserveCommand($event)"
/>
</div>
</div>
<div class="commandPad chiefReservedCommand">
<div :class="['commandTable', isEditMode ? 'editMode' : 'singleMode']">
<DragSelect
v-slot="{ selected }"
:style="rowGridStyle"
attribute="turnIdx"
:disabled="!isEditMode"
@dragStart="isDragSingle = true"
@dragDone="
isDragSingle = false;
queryActionHelper.selectTurn(...$event);
isDragSingle = false;
queryActionHelper.selectTurn(...$event);
"
v-slot="{ selected }"
>
<div
v-for="(turnObj, turnIdx) in reservedCommandList"
@@ -166,21 +147,22 @@ queryActionHelper.selectTurn(...$event);
backgroundColor: 'black',
whiteSpace: 'nowrap',
overflow: 'hidden',
color:
isDragSingle && selected.has(`${turnIdx}`) ? 'cyan' : undefined,
color: isDragSingle && selected.has(`${turnIdx}`) ? 'cyan' : undefined,
}"
>{{ turnObj.time }}</div>
>
{{ turnObj.time }}
</div>
</DragSelect>
<DragSelect
v-slot="{ selected }"
:style="{ ...rowGridStyle, display: isEditMode ? 'grid' : 'none' }"
attribute="turnIdx"
:disabled="!isEditMode"
@dragStart="isDragToggle = true"
@dragDone="
isDragToggle = false;
toggleTurn(...$event);
isDragToggle = false;
toggleTurn(...$event);
"
v-slot="{ selected }"
>
<div
v-for="(turnObj, turnIdx) in reservedCommandList"
@@ -194,90 +176,75 @@ toggleTurn(...$event);
isDragToggle && selected.has(`${turnIdx}`)
? 'light'
: selectedTurnList.has(turnIdx)
? 'info'
: selectedTurnList.size == 0 && prevSelectedTurnList.has(turnIdx)
? 'success'
: 'primary'
? 'info'
: selectedTurnList.size == 0 && prevSelectedTurnList.has(turnIdx)
? 'success'
: 'primary'
"
>{{ turnIdx + 1 }}</BButton>
>
{{ turnIdx + 1 }}
</BButton>
</div>
</DragSelect>
<div :style="rowGridStyle">
<div
v-for="(turnObj, turnIdx) in reservedCommandList"
:key="turnIdx"
class="turn_pad center"
>
<span
class="turn_text"
:style="turnObj.style"
v-b-tooltip.hover
:title="turnObj.tooltip"
v-html="turnObj.brief"
></span>
<div v-for="(turnObj, turnIdx) in reservedCommandList" :key="turnIdx" class="turn_pad center">
<span v-b-tooltip.hover class="turn_text" :style="turnObj.style" :title="turnObj.tooltip">
<!-- eslint-disable-next-line vue/no-v-html -->
<span v-html="turnObj.brief" />
</span>
</div>
</div>
<div :style="{ ...rowGridStyle, display: isEditMode ? 'none' : 'grid' }">
<div v-for="turnIdx in range(props.maxTurn)" :key="turnIdx" class="action_pad d-grid">
<BButton
:variant="(turnIdx % 2 == 0) ? 'secondary' : 'dark'"
:variant="turnIdx % 2 == 0 ? 'secondary' : 'dark'"
size="sm"
class="simple_action_btn bi bi-pencil"
@click="toggleQuickReserveForm(turnIdx)"
></BButton>
/>
</div>
</div>
</div>
<div style="position:relative">
<div style="position: relative">
<CommandSelectForm
:commandList="commandList"
ref="commandSelectForm"
@on-close="chooseCommand($event)"
v-model:activatedCategory="activatedCategory"
:commandList="commandList"
class="bg-dark"
:style="{ position: 'absolute', bottom: '0' }"
@onClose="chooseCommand($event)"
/>
</div>
<div class="row gx-0" v-if="isEditMode">
<div v-if="isEditMode" class="row gx-0">
<div class="col-5 col-md-6 d-grid">
<BDropdown left variant="info" text="선택한 턴을">
<BDropdownItem @click="clipboardCut">
<i class="bi bi-scissors"></i>&nbsp;잘라내기
</BDropdownItem>
<BDropdownItem @click="clipboardCopy">
<i class="bi bi-files"></i>&nbsp;복사하기
</BDropdownItem>
<BDropdownItem @click="clipboardCut"> <i class="bi bi-scissors" />&nbsp;잘라내기 </BDropdownItem>
<BDropdownItem @click="clipboardCopy"> <i class="bi bi-files" />&nbsp;복사하기 </BDropdownItem>
<BDropdownItem @click="clipboardPaste">
<i class="bi bi-clipboard-fill"></i>&nbsp;붙여넣기
<i class="bi bi-clipboard-fill" />&nbsp;붙여넣기
</BDropdownItem>
<BDropdownDivider />
<BDropdownItem @click="setStoredActions">
<i class="bi bi-bookmark-plus-fill"></i>&nbsp;보관하기
<i class="bi bi-bookmark-plus-fill" />&nbsp;보관하기
</BDropdownItem>
<BDropdownItem @click="subRepeatCommand">
<i class="bi bi-arrow-repeat"></i>&nbsp;반복하기
<i class="bi bi-arrow-repeat" />&nbsp;반복하기
</BDropdownItem>
<BDropdownDivider />
<BDropdownItem @click="eraseSelectedTurnList">
<i class="bi bi-eraser"></i>&nbsp;비우기
</BDropdownItem>
<BDropdownItem @click="eraseSelectedTurnList"> <i class="bi bi-eraser" />&nbsp;비우기 </BDropdownItem>
<BDropdownItem @click="eraseAndPullCommand">
<i class="bi bi-arrow-bar-up"></i>&nbsp;지우고 당기기
<i class="bi bi-arrow-bar-up" />&nbsp;지우고 당기기
</BDropdownItem>
<BDropdownItem @click="pushEmptyCommand">
<i class="bi bi-arrow-bar-down"></i>&nbsp;뒤로 밀기
<i class="bi bi-arrow-bar-down" />&nbsp;뒤로 밀기
</BDropdownItem>
<!-- 최근에 실행한 10 -->
</BDropdown>
</div>
<div class="col-7 col-md-6 d-grid">
<BButton
variant="light"
@click="toggleForm($event)"
:style="{ color: 'black' }"
>명령 선택 </BButton>
<BButton variant="light" :style="{ color: 'black' }" @click="toggleForm($event)"> 명령 선택 </BButton>
</div>
</div>
</div>
@@ -327,7 +294,6 @@ const props = defineProps({
turnTime: VueTypes.string.isRequired,
targetIsMe: VueTypes.bool.isRequired,
selectedTurn: {
type: Object as PropType<Set<number>>,
required: false,
@@ -338,15 +304,15 @@ const props = defineProps({
required: true,
},
commandList: {
type: Object as PropType<ChiefResponse['commandList']>,
type: Object as PropType<ChiefResponse["commandList"]>,
required: true,
},
officer: {
type: Object as PropType<ChiefResponse['chiefList'][0]>,
type: Object as PropType<ChiefResponse["chiefList"][0]>,
required: true,
}
})
},
});
const basicModeRowHeight = 30;
@@ -391,8 +357,8 @@ const isDragToggle = ref(false);
const autorun_limit = ref<number | null>(null);
const emit = defineEmits<{
(event: 'raiseReload'): void,
(event: 'update:selectedTurn', value: Set<number>): void,
(event: "raise-reload"): void;
(event: "update:selectedTurn", value: Set<number>): void;
}>();
function triggerUpdateCommandList(type?: string) {
@@ -442,10 +408,9 @@ async function repeatNationCommand(amount: number) {
alert(`실패했습니다: ${e}`);
return;
}
emit('raiseReload');
emit("raise-reload");
}
function pushNationCommandSingle(e: Event) {
//NOTE: split 구현에 버그가 있어서, 수동으로 구분해야함
if (isDropdownChildren(e)) {
@@ -470,10 +435,9 @@ async function pushNationCommand(amount: number) {
alert(`실패했습니다: ${e}`);
return;
}
emit('raiseReload');
emit("raise-reload");
}
const queryActionHelper = new QueryActionHelper(props.maxTurn);
const reservedCommandList = queryActionHelper.reservedCommandList;
const prevSelectedTurnList = queryActionHelper.prevSelectedTurnList;
@@ -481,15 +445,15 @@ const selectedTurnList = queryActionHelper.selectedTurnList;
async function reserveCommandDirect(args: [number[], TurnObj][], reload = true): Promise<boolean> {
const query: {
turnList: number[],
action: string,
arg: Args
turnList: number[];
action: string;
arg: Args;
}[] = [];
for (const [turnList, { action, arg }] of args) {
query.push({
turnList,
action,
arg
arg,
});
}
@@ -503,7 +467,7 @@ async function reserveCommandDirect(args: [number[], TurnObj][], reload = true):
}
if (reload) {
emit('raiseReload');
emit("raise-reload");
}
return true;
}
@@ -520,9 +484,7 @@ function updateCommandList() {
let nextTurnTime = new Date(turnTime);
const autorunLimitYearMonth = autorun_limit.value ?? yearMonth - 1;
const [autorunLimitYear, autorunLimitMonth] = parseYearMonth(
autorunLimitYearMonth
);
const [autorunLimitYear, autorunLimitMonth] = parseYearMonth(autorunLimitYearMonth);
for (const obj of props.turn) {
const [year, month] = parseYearMonth(yearMonth);
@@ -537,9 +499,7 @@ function updateCommandList() {
}
style.color = "#aaffff";
tooltip.push(
`자율 행동 기간: ${autorunLimitYear}${autorunLimitMonth}월까지`
);
tooltip.push(`자율 행동 기간: ${autorunLimitYear}${autorunLimitMonth}월까지`);
}
if (mb_strwidth(brief) > 22) {
@@ -550,10 +510,7 @@ function updateCommandList() {
...obj,
year,
month,
time: formatTime(
nextTurnTime,
props.turnTerm >= 5 ? "HH:mm" : "mm:ss"
),
time: formatTime(nextTurnTime, props.turnTerm >= 5 ? "HH:mm" : "mm:ss"),
tooltip: tooltip.length == 0 ? undefined : tooltip.join("\n"),
style,
});
@@ -590,7 +547,7 @@ async function reserveCommand() {
storedActionsHelper.pushRecentActions({
action: commandName,
brief: result.brief,
arg: {}
arg: {},
});
queryActionHelper.releaseSelectedTurnList();
@@ -599,7 +556,7 @@ async function reserveCommand() {
alert(`실패했습니다: ${e}`);
return;
}
emit("raiseReload");
emit("raise-reload");
}
function chooseCommand(val?: string) {
@@ -610,20 +567,16 @@ function chooseCommand(val?: string) {
void reserveCommand();
}
const emptyTurnObj: TurnObj = { action: '휴식', brief: '휴식', arg: {} };
const emptyTurnObj: TurnObj = { action: "휴식", brief: "휴식", arg: {} };
const storedActionsHelper = inject('storedNationActionsHelper') as StoredActionsHelper;
const storedActionsHelper = inject("storedNationActionsHelper") as StoredActionsHelper;
const recentActions = storedActionsHelper.recentActions;
const storedActions = storedActionsHelper.storedActions;
const activatedCategory = storedActionsHelper.activatedCategory;
async function eraseSelectedTurnList(releaseSelect = true): Promise<boolean> {
const result = await reserveCommandDirect([[
queryActionHelper.getSelectedTurnList(),
emptyTurnObj
]]);
const result = await reserveCommandDirect([[queryActionHelper.getSelectedTurnList(), emptyTurnObj]]);
if (releaseSelect) {
queryActionHelper.releaseSelectedTurnList();
}
@@ -669,7 +622,10 @@ async function subRepeatCommand(releaseSelect = true): Promise<boolean> {
const queryLength = selectedMaxTurnIdx - selectedMinTurnIdx + 1;
const rawActions = queryActionHelper.extractQueryActions();
const actions = queryActionHelper.amplifyQueryActions(rawActions, range(selectedMinTurnIdx, props.maxTurn, queryLength));
const actions = queryActionHelper.amplifyQueryActions(
rawActions,
range(selectedMinTurnIdx, props.maxTurn, queryLength)
);
const result = await reserveCommandDirect(actions);
if (releaseSelect) {
@@ -678,7 +634,6 @@ async function subRepeatCommand(releaseSelect = true): Promise<boolean> {
return result;
}
async function eraseAndPullCommand(releaseSelect = true): Promise<boolean> {
const reqTurnList = queryActionHelper.getSelectedTurnList();
const selectedMinTurnIdx = reqTurnList[0];
@@ -696,7 +651,6 @@ async function eraseAndPullCommand(releaseSelect = true): Promise<boolean> {
const actions: [number[], TurnObj][] = [];
const emptyTurnList: number[] = [];
for (const srcTurnIdx of range(selectedMinTurnIdx + queryLength, props.maxTurn)) {
@@ -705,11 +659,14 @@ async function eraseAndPullCommand(releaseSelect = true): Promise<boolean> {
emptyTurnList.push(srcTurnIdx - queryLength);
continue;
}
actions.push([[srcTurnIdx - queryLength], {
action: rawAction.action,
arg: rawAction.arg,
brief: rawAction.brief
}]);
actions.push([
[srcTurnIdx - queryLength],
{
action: rawAction.action,
arg: rawAction.arg,
brief: rawAction.brief,
},
]);
}
emptyTurnList.push(...range(props.maxTurn - queryLength, props.maxTurn));
@@ -739,7 +696,6 @@ async function pushEmptyCommand(releaseSelect = true): Promise<boolean> {
const actions: [number[], TurnObj][] = [];
const emptyTurnList: number[] = [];
for (const srcTurnIdx of range(selectedMinTurnIdx, props.maxTurn - queryLength)) {
@@ -748,11 +704,14 @@ async function pushEmptyCommand(releaseSelect = true): Promise<boolean> {
emptyTurnList.push(srcTurnIdx + queryLength);
continue;
}
actions.push([[srcTurnIdx + queryLength], {
action: rawAction.action,
arg: rawAction.arg,
brief: rawAction.brief
}]);
actions.push([
[srcTurnIdx + queryLength],
{
action: rawAction.action,
arg: rawAction.arg,
brief: rawAction.brief,
},
]);
}
emptyTurnList.push(...range(selectedMinTurnIdx, selectedMinTurnIdx + queryLength));
@@ -769,7 +728,7 @@ function setStoredActions() {
const actions = queryActionHelper.extractQueryActions();
const turnBrief = new Map<number, string>();
for (const [subTurnList, action] of actions) {
const actionName = action.action.split('_');
const actionName = action.action.split("_");
const actionShortName = actionName.length == 1 ? actionName[0] : actionName[1];
for (const turnIdx of subTurnList) {
turnBrief.set(turnIdx, actionShortName[0]);
@@ -779,10 +738,10 @@ function setStoredActions() {
const turnBriefStr = Array.from(turnBrief.entries())
.sort(([turnA], [turnB]) => turnA - turnB)
.map(([, action]) => action)
.join('');
.join("");
const nickName = trim(prompt('선택한 턴들의 별명을 지어주세요', turnBriefStr) ?? '');
if (nickName == '') {
const nickName = trim(prompt("선택한 턴들의 별명을 지어주세요", turnBriefStr) ?? "");
if (nickName == "") {
return;
}
@@ -791,12 +750,12 @@ function setStoredActions() {
}
function deleteStoredActions(actionKey: string) {
storedActionsHelper.deleteStoredActions(actionKey)
storedActionsHelper.deleteStoredActions(actionKey);
}
async function useStoredAction(rawActions: [number[], TurnObj][]) {
const reqTurnList = queryActionHelper.getSelectedTurnList();
const actions = queryActionHelper.amplifyQueryActions(rawActions, reqTurnList)
const actions = queryActionHelper.amplifyQueryActions(rawActions, reqTurnList);
const result = await reserveCommandDirect(actions);
queryActionHelper.releaseSelectedTurnList();
return result;
@@ -818,39 +777,57 @@ defineExpose({
clipboardPaste,
getQueryActionHelper,
getStoredActionHeler,
})
});
watch(() => props.date, () => {
triggerUpdateCommandList("date");
})
watch(() => props.year, () => {
triggerUpdateCommandList("year");
})
watch(() => props.month, () => {
triggerUpdateCommandList("month");
})
watch(() => props.turnTime, () => {
triggerUpdateCommandList("turnTime");
})
watch(() => props.commandList, () => {
triggerUpdateCommandList("commandList");
})
watch(() => props.selectedTurn, (val: Set<number>) => {
console.log(val);
if (val === selectedTurnList.value) {
console.log("pass!");
return;
watch(
() => props.date,
() => {
triggerUpdateCommandList("date");
}
selectedTurnList.value.clear();
for (const t of val.values()) {
selectedTurnList.value.add(t);
);
watch(
() => props.year,
() => {
triggerUpdateCommandList("year");
}
})
);
watch(
() => props.month,
() => {
triggerUpdateCommandList("month");
}
);
watch(
() => props.turnTime,
() => {
triggerUpdateCommandList("turnTime");
}
);
watch(
() => props.commandList,
() => {
triggerUpdateCommandList("commandList");
}
);
watch(
() => props.selectedTurn,
(val: Set<number>) => {
console.log(val);
if (val === selectedTurnList.value) {
console.log("pass!");
return;
}
selectedTurnList.value.clear();
for (const t of val.values()) {
selectedTurnList.value.add(t);
}
}
);
watch(selectedTurnList, () => {
console.log(selectedTurnList.value);
emit("update:selectedTurn", selectedTurnList.value);
})
});
const commandQuickReserveForm = ref<InstanceType<typeof CommandSelectForm> | null>(null);
const commandSelectForm = ref<InstanceType<typeof CommandSelectForm> | null>(null);
@@ -876,12 +853,11 @@ function toggleQuickReserveForm(turnIdx: number) {
}
const isEditMode = storedActionsHelper.isEditMode;
watch(isEditMode, newEditMode => {
watch(isEditMode, (newEditMode) => {
if (newEditMode) {
commandQuickReserveForm.value?.close();
currentQuickReserveTarget.value = -1;
}
else {
} else {
commandSelectForm.value?.close();
}
});
@@ -897,8 +873,7 @@ function toggleForm($event: Event): void {
onMounted(() => {
updateCommandList();
})
});
</script>
<style lang="scss">
@import "@scss/common/break_500px.scss";
@@ -990,4 +965,4 @@ onMounted(() => {
overflow: hidden;
}
}
</style>
</style>
+153 -155
View File
@@ -1,68 +1,58 @@
<template>
<div v-if="showForm" class="my-1">
<div class="commandCategory row gx-0 gy-1">
<div
class="categoryItem col-4 d-grid"
v-for="[categoryKey, { deco: categoryDeco }] of commandList"
:key="categoryKey"
>
<BButton
variant="success"
@click="chosenCategory = categoryKey"
:active="chosenCategory == categoryKey"
>{{ categoryDeco.altName ?? categoryDeco.name }}</BButton>
</div>
</div>
<div
class="commandList my-1"
:style="{
display: 'grid',
alignItems: 'self-start',
}"
>
<div
class="row gx-1 gy-1"
v-for="[category, { values }] of commandList"
:key="category"
:style="{ visibility: category == chosenCategory ? 'visible' : 'hidden', gridRow: '1', gridColumn: '1' }"
>
<div
class="col-6 d-grid"
v-for="commandItem of values"
:key="commandItem.value"
@click="close(commandItem.value)"
>
<div class="commandItem">
<p
:class="['center', 'my-0', commandItem.possible ? '' : 'commandImpossible']"
>
{{ commandItem.simpleName }}
<span
class="compensatePositive"
v-if="commandItem.compensation > 0"
></span>
<span
class="compensateNegative"
v-else-if="commandItem.compensation < 0"
></span>
</p>
<small class="center" :style="{ display: 'block' }">
{{
commandItem.title.startsWith(commandItem.simpleName)
? commandItem.title.substring(commandItem.simpleName.length)
: commandItem.title
}}
</small>
</div>
</div>
</div>
</div>
<div v-if="!hideClose" class="commandBottom row mt-1 mb-1">
<div class="offset-8 col-4 d-grid">
<BButton @click="close()">닫기</BButton>
</div>
</div>
<div v-if="showForm" class="my-1">
<div class="commandCategory row gx-0 gy-1">
<div
v-for="[categoryKey, { deco: categoryDeco }] of commandList"
:key="categoryKey"
class="categoryItem col-4 d-grid"
>
<BButton variant="success" :active="chosenCategory == categoryKey" @click="chosenCategory = categoryKey">
{{ categoryDeco.altName ?? categoryDeco.name }}
</BButton>
</div>
</div>
<div
class="commandList my-1"
:style="{
display: 'grid',
alignItems: 'self-start',
}"
>
<div
v-for="[category, { values }] of commandList"
:key="category"
class="row gx-1 gy-1"
:style="{ visibility: category == chosenCategory ? 'visible' : 'hidden', gridRow: '1', gridColumn: '1' }"
>
<div
v-for="commandItem of values"
:key="commandItem.value"
class="col-6 d-grid"
@click="close(commandItem.value)"
>
<div class="commandItem">
<p :class="['center', 'my-0', commandItem.possible ? '' : 'commandImpossible']">
{{ commandItem.simpleName }}
<span v-if="commandItem.compensation > 0" class="compensatePositive"></span>
<span v-else-if="commandItem.compensation < 0" class="compensateNegative"></span>
</p>
<small class="center" :style="{ display: 'block' }">
{{
commandItem.title.startsWith(commandItem.simpleName)
? commandItem.title.substring(commandItem.simpleName.length)
: commandItem.title
}}
</small>
</div>
</div>
</div>
</div>
<div v-if="!hideClose" class="commandBottom row mt-1 mb-1">
<div class="offset-8 col-4 d-grid">
<BButton @click="close()"> 닫기 </BButton>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { CommandItem } from "@/defs";
@@ -70,144 +60,152 @@ import { BButton } from "bootstrap-vue-3";
import { ref, type PropType, watch, onMounted } from "vue";
interface CategoryDecoration {
name: string,
altName?: string,
//icon?: string,
//color?: string,
//backgroundColor?: string,
name: string;
altName?: string;
//icon?: string,
//color?: string,
//backgroundColor?: string,
}
const props = defineProps({
categoryInfo: {
type: Object as PropType<Record<string, Omit<CategoryDecoration, 'name'>>>,
required: false,
categoryInfo: {
type: Object as PropType<Record<string, Omit<CategoryDecoration, "name">>>,
default: () => {
return {};
},
commandList: {
type: Object as PropType<{
category: string;
values: CommandItem[];
}[]>,
required: true,
},
anchor: {
type: String,
required: false,
default: '.commandSelectFormAnchor',
},
hideClose: {
type: Boolean,
required: false,
default: true,
},
activatedCategory: {
type: String,
required: false,
default: "",
}
})
required: false,
},
commandList: {
type: Object as PropType<
{
category: string;
values: CommandItem[];
}[]
>,
required: true,
},
anchor: {
type: String,
required: false,
default: ".commandSelectFormAnchor",
},
hideClose: {
type: Boolean,
required: false,
default: true,
},
activatedCategory: {
type: String,
required: false,
default: "",
},
});
const chosenCategory = ref<string>('-');
const chosenCategory = ref<string>("-");
const chosenSubList = ref<CommandItem[]>([]);
const categories = new Set(props.commandList.map(({ category }) => category));
watch(() => props.activatedCategory, (newValue) => {
watch(
() => props.activatedCategory,
(newValue) => {
chosenCategory.value = newValue;
})
}
);
const showForm = ref(false);
function convCategoryDeco(category: string): CategoryDecoration {
const itemInfo = props.categoryInfo?.[category];
if (!itemInfo) {
return {
name: category,
}
}
const itemInfo = props.categoryInfo?.[category];
if (!itemInfo) {
return {
name: category,
...itemInfo
name: category,
};
}
return {
name: category,
...itemInfo,
};
}
const commandList = ref(new Map<string, {
deco: CategoryDecoration,
values: CommandItem[],
}>());
const commandList = ref(
new Map<
string,
{
deco: CategoryDecoration;
values: CommandItem[];
}
>()
);
function updateCommandList(rawCommandList: typeof props.commandList) {
commandList.value.clear();
for (const { category, values } of rawCommandList) {
commandList.value.set(category, {
deco: convCategoryDeco(category),
values
});
}
commandList.value.clear();
for (const { category, values } of rawCommandList) {
commandList.value.set(category, {
deco: convCategoryDeco(category),
values,
});
}
}
watch(() => props.commandList, updateCommandList);
updateCommandList(props.commandList);
watch(chosenCategory, (category) => {
const itemInfo = commandList.value?.get(category);
if (itemInfo === undefined) {
console.error(`category 없음: ${category}`);
return;
}
chosenSubList.value = itemInfo.values;
if (props.activatedCategory !== category) {
emits('update:activatedCategory', category);
}
const itemInfo = commandList.value?.get(category);
if (itemInfo === undefined) {
console.error(`category 없음: ${category}`);
return;
}
chosenSubList.value = itemInfo.values;
if (props.activatedCategory !== category) {
emits("update:activatedCategory", category);
}
});
onMounted(() => {
if (!categories.has(props.activatedCategory)) {
chosenCategory.value = props.commandList[0].category;
}
else {
chosenCategory.value = props.activatedCategory;
}
if (!categories.has(props.activatedCategory)) {
chosenCategory.value = props.commandList[0].category;
} else {
chosenCategory.value = props.activatedCategory;
}
});
function show(): void {
showForm.value = true;
showForm.value = true;
}
function toggle(): void {
showForm.value = !showForm.value;
if (showForm.value === false) {
emits('onClose');
}
showForm.value = !showForm.value;
if (showForm.value === false) {
emits("onClose");
}
}
function close(category?: string): void {
showForm.value = false;
emits('onClose', category);
showForm.value = false;
emits("onClose", category);
}
const emits = defineEmits<{
(event: 'onClose', command?: string): void,
(event: 'update:activatedCategory', category: string): void,
(event: "onClose", command?: string): void;
(event: "update:activatedCategory", category: string): void;
}>();
defineExpose({
show,
close,
toggle
})
show,
close,
toggle,
});
</script>
<style scoped>
.commandItem {
border: gray 1px solid;
border-radius: 0.5em;
overflow: hidden;
cursor: pointer;
padding: 0.1em;
margin: 0;
border: gray 1px solid;
border-radius: 0.5em;
overflow: hidden;
cursor: pointer;
padding: 0.1em;
margin: 0;
}
</style>
</style>
+21 -34
View File
@@ -6,8 +6,7 @@
userSelect: disabled ? undefined : 'none',
overflow: 'hidden',
touchAction: disabled ? undefined : 'none',
}
"
}"
:class="{ disabledDrag: disabled }"
>
<slot :selected="intersected" />
@@ -16,14 +15,7 @@
<script lang="ts">
/// https://github.com/andi23rosca/drag-select-vue/blob/master/src/DragSelect.vue
import {
defineComponent,
ref,
watch,
onMounted,
onBeforeUnmount,
type PropType,
} from "vue";
import { defineComponent, ref, watch, onMounted, onBeforeUnmount, type PropType } from "vue";
import VueTypes from "vue-types";
function getDimensions(p1: coord, p2: coord): rect {
@@ -47,14 +39,8 @@ type rect = { width: number; height: number };
export default defineComponent({
props: {
attribute: VueTypes.string.isRequired,
color: {
...VueTypes.string.def("#4299E1"),
required: false,
},
opacity: {
...VueTypes.number.def(0.7),
required: false,
},
color: VueTypes.string.def("#4299E1"),
opacity: VueTypes.number.def(0.7),
modelValue: {
type: Object as PropType<Set<string>>,
required: false,
@@ -64,7 +50,7 @@ export default defineComponent({
type: Boolean,
required: false,
default: false,
}
},
},
emits: ["update:modelValue", "dragDone", "dragStart"],
setup(props, { emit }) {
@@ -194,20 +180,22 @@ export default defineComponent({
isMine = false;
}
watch(() => props.disabled, disabledNext => {
if (disabledNext) {
uContainer.removeEventListener("mousedown", startDrag);
uContainer.removeEventListener("touchstart", touchStart);
document.removeEventListener("mouseup", endDrag);
document.removeEventListener("touchend", endDrag);
watch(
() => props.disabled,
(disabledNext) => {
if (disabledNext) {
uContainer.removeEventListener("mousedown", startDrag);
uContainer.removeEventListener("touchstart", touchStart);
document.removeEventListener("mouseup", endDrag);
document.removeEventListener("touchend", endDrag);
} else {
uContainer.addEventListener("mousedown", startDrag);
uContainer.addEventListener("touchstart", touchStart);
document.addEventListener("mouseup", endDrag);
document.addEventListener("touchend", endDrag);
}
}
else {
uContainer.addEventListener("mousedown", startDrag);
uContainer.addEventListener("touchstart", touchStart);
document.addEventListener("mouseup", endDrag);
document.addEventListener("touchend", endDrag);
}
});
);
if (!props.disabled) {
uContainer.addEventListener("mousedown", startDrag);
@@ -216,7 +204,6 @@ export default defineComponent({
document.addEventListener("touchend", endDrag);
}
onBeforeUnmount(() => {
uContainer.removeEventListener("mousedown", startDrag);
uContainer.removeEventListener("touchstart", touchStart);
@@ -231,4 +218,4 @@ export default defineComponent({
};
},
});
</script>
</script>
+11 -12
View File
@@ -1,21 +1,18 @@
<template>
<div
:id="uuid"
:class="['world_map', `map_theme_${mapTheme}`, 'draw_required']"
>
<div :id="uuid" :class="['world_map', `map_theme_${mapName}`, 'draw_required']">
<div
class="map_title obj_tooltip"
data-bs-toggle="tooltip"
data-bs-placement="top"
data-tooltip-class="map_title_tooltiptext"
>
<span class="map_title_text"> </span>
<span class="tooltiptext"></span>
<span class="map_title_text" />
<span class="tooltiptext" />
</div>
<div class="map_body">
<div class="map_bglayer1"></div>
<div class="map_bglayer2"></div>
<div class="map_bgroad"></div>
<div class="map_bglayer1" />
<div class="map_bglayer2" />
<div class="map_bgroad" />
<div class="map_button_stack">
<button
type="button"
@@ -38,8 +35,8 @@
</div>
</div>
<div class="city_tooltip">
<div class="city_name"></div>
<div class="nation_name"></div>
<div class="city_name" />
<div class="nation_name" />
</div>
</div>
</template>
@@ -51,7 +48,7 @@ import { v4 as uuidv4 } from "uuid";
export type { MapCityParsed };
export default defineComponent({
props: {
mapTheme: {
mapName: {
type: String,
required: true,
},
@@ -59,6 +56,7 @@ export default defineComponent({
clickableAll: { type: Boolean, default: undefined, required: false },
selectCallback: {
type: Function as PropType<loadMapOption["selectCallback"]>,
default: undefined,
required: false,
},
hrefTemplate: { type: String, default: undefined, required: false },
@@ -88,6 +86,7 @@ export default defineComponent({
modelValue: {
type: Object as PropType<MapCityParsed>,
default: undefined,
required: false,
},
},
+13 -11
View File
@@ -1,32 +1,32 @@
<template>
<div class="row form-group number-input-with-info">
<label v-if="!right" class="col-6 col-form-label ">{{ title }}</label>
<label v-if="!right" class="col-6 col-form-label">{{ title }}</label>
<div class="col-6">
<input
ref="input"
v-model="rawValue"
type="number"
:step="step ?? undefined"
v-model="rawValue"
class="form-control f_tnum"
:min="min ?? undefined"
:max="max ?? undefined"
:style="{ display: editmode ? undefined : 'none' }"
@blur="onBlurNumber"
@input="updateValue"
:style="{ display: editmode ? undefined : 'none' }"
/>
<input
type="text"
class="form-control f_tnum"
:readonly="readonly"
:value="printValue"
@focus="onFocusText"
:style="{ display: !editmode ? undefined : 'none' }"
@focus="onFocusText"
/>
</div>
<label v-if="right" class="col-6 col-form-label">{{ title }}</label>
</div>
<div style="text-align: right">
<small class="form-text text-muted"><slot></slot></small>
<small class="form-text text-muted"><slot /></small>
</div>
</template>
<script lang="ts">
@@ -56,10 +56,12 @@ export default defineComponent({
},
max: {
type: Number,
default: undefined,
required: false,
},
step: {
type: Number,
default: undefined,
required: false,
},
modelValue: {
@@ -70,7 +72,7 @@ export default defineComponent({
type: Boolean,
required: false,
default: false,
}
},
},
emits: ["update:modelValue"],
data() {
@@ -80,15 +82,15 @@ export default defineComponent({
printValue: this.modelValue.toLocaleString(),
};
},
watch:{
modelValue: function(newVal:number){
watch: {
modelValue: function (newVal: number) {
this.rawValue = newVal;
this.printValue = newVal.toLocaleString();
}
},
},
methods: {
updateValue() {
if(this.readonly){
if (this.readonly) {
return;
}
if (this.int) {
@@ -102,7 +104,7 @@ export default defineComponent({
this.printValue = this.rawValue.toLocaleString();
},
onFocusText() {
if(this.readonly){
if (this.readonly) {
return;
}
this.editmode = true;
+25 -22
View File
@@ -1,31 +1,34 @@
<template>
<span class="time-zone">{{serverNow}}</span>
<span class="time-zone">{{ serverNow }}</span>
</template>
<script lang="ts" setup>
import { addMilliseconds } from 'date-fns';
import { type PropType, ref, onMounted, watch } from 'vue';
import { formatTime } from '@/util/formatTime';
import { addMilliseconds } from "date-fns";
import { type PropType, ref, onMounted, watch } from "vue";
import { formatTime } from "@/util/formatTime";
const props = defineProps({
serverTime: {
type: Object as PropType<Date>,
required: false,
default: new Date(),
},
timeFormat: {
type: String,
required: false,
default: 'HH:mm:ss'
}
})
serverTime: {
type: Object as PropType<Date>,
required: false,
default: new Date(),
},
timeFormat: {
type: String,
required: false,
default: "HH:mm:ss",
},
});
const timeDiff = ref(0);
const serverNow = ref('');
const serverNow = ref("");
watch(()=>props.serverTime, (newValue)=>{
watch(
() => props.serverTime,
(newValue) => {
const clientNow = new Date();
timeDiff.value = newValue.getTime() - clientNow.getTime();
});
}
);
function updateNow() {
const serverNowObj = addMilliseconds(new Date(), timeDiff.value);
@@ -36,8 +39,8 @@ function updateNow() {
}
onMounted(() => {
const clientNow = new Date();
timeDiff.value = props.serverTime.getTime() - clientNow.getTime();
updateNow();
const clientNow = new Date();
timeDiff.value = props.serverTime.getTime() - clientNow.getTime();
updateNow();
});
</script>
</script>
+326 -346
View File
@@ -1,53 +1,54 @@
<template>
<b-button-toolbar key-nav v-if="editable && editor" class="bg-dark">
<b-button-group class="mx-1">
<b-button
@click="editor.commands.undo()"
<BButtonToolbar v-if="editable && editor" key-nav class="bg-dark">
<BButtonGroup class="mx-1">
<BButton v-b-tooltip.hover title="되돌리기" @click="editor?.commands.undo()">
<i class="bi bi-arrow-90deg-left" />
</BButton>
<BButton v-b-tooltip.hover title="재실행" @click="editor?.commands.redo()">
<i class="bi bi-arrow-90deg-right" />
</BButton>
</BButtonGroup>
<BButtonGroup class="mx-1">
<BButton
v-b-tooltip.hover
title="되돌리기"
><i class="bi bi-arrow-90deg-left"></i
></b-button>
<b-button @click="editor.commands.redo()" v-b-tooltip.hover title="재실행"
><i class="bi bi-arrow-90deg-right"></i
></b-button>
</b-button-group>
<b-button-group class="mx-1">
<b-button
@click="editor.chain().focus().toggleBold().run()"
:class="{ 'is-active': editor.isActive('bold') }"
v-b-tooltip.hover
title="진하게"
><i class="bi bi-type-bold"></i
></b-button>
<b-button
@click="editor.chain().focus().toggleItalic().run()"
@click="editor?.chain().focus().toggleBold().run()"
>
<i class="bi bi-type-bold" />
</BButton>
<BButton
v-b-tooltip.hover
:class="{ 'is-active': editor.isActive('italic') }"
v-b-tooltip.hover
title="기울이기"
><i class="bi bi-type-italic"></i
></b-button>
<b-button
@click="editor.chain().focus().toggleUnderline().run()"
:class="{ 'is-active': editor.isActive('underline') }"
@click="editor?.chain().focus().toggleItalic().run()"
>
<i class="bi bi-type-italic" />
</BButton>
<BButton
v-b-tooltip.hover
:class="{ 'is-active': editor.isActive('underline') }"
title="밑줄"
><i class="bi bi-type-underline"></i
></b-button>
@click="editor?.chain().focus().toggleUnderline().run()"
>
<i class="bi bi-type-underline" />
</BButton>
<!-- 효과 지우기 -->
</b-button-group>
</BButtonGroup>
<b-button-group class="mx-1">
<b-dropdown>
<BButtonGroup class="mx-1">
<BDropdown>
<template #button-content> 크기 </template>
<b-dropdown-item @click="editor.chain().focus().unsetFontSize().run()"
><span>기본</span></b-dropdown-item
>
<b-dropdown-divider />
<b-dropdown-item
<BDropdownItem @click="editor?.chain().focus().unsetFontSize().run()">
<span>기본</span>
</BDropdownItem>
<BDropdownDivider />
<BDropdownItem
v-for="sizeItem in fontSize"
:key="sizeItem"
@click="editor.chain().focus().setFontSize(sizeItem).run()"
><span
@click="editor?.chain().focus().setFontSize(sizeItem).run()"
>
<span
:style="{
'font-size': sizeItem,
'text-decoration': editor.isActive('textStyle', {
@@ -57,235 +58,230 @@
: undefined,
}"
>{{ sizeItem }}</span
></b-dropdown-item
>
</b-dropdown>
>
</BDropdownItem>
</BDropdown>
<!-- 글꼴 -->
</b-button-group>
</BButtonGroup>
<b-button-group class="mx-1">
<b-button
@click="editor.chain().focus().toggleStrike().run()"
:class="{ 'is-active': editor.isActive('strike') }"
<BButtonGroup class="mx-1">
<BButton
v-b-tooltip.hover
:class="{ 'is-active': editor.isActive('strike') }"
title="가로선"
><i class="bi bi-type-strikethrough"></i
></b-button>
@click="editor?.chain().focus().toggleStrike().run()"
>
<i class="bi bi-type-strikethrough" />
</BButton>
<!-- 윗첨자, 아랫첨자 -->
</b-button-group>
</BButtonGroup>
<b-button-group class="mx-1">
<b-button
@click="
editor.chain().focus().unsetColor().unsetBackgroundColor().run()
"
<BButtonGroup class="mx-1">
<BButton
v-b-tooltip.hover
title="색상 취소"
><i class="bi bi-droplet"></i
></b-button>
@click="editor?.chain().focus().unsetColor().unsetBackgroundColor().run()"
>
<i class="bi bi-droplet" />
</BButton>
<input
v-b-tooltip.hover
type="color"
class="form-control form-control-color"
:value="
colorConvert(editor.getAttributes('textStyle').color, '#ffffff')
"
@input="editor.chain().focus().setColor(($event.target as HTMLInputElement).value).run()"
v-b-tooltip.hover
:value="colorConvert(editor.getAttributes('textStyle').color, '#ffffff')"
title="글자색"
@input="editor?.chain().focus().setColor(($event.target as HTMLInputElement).value).run()"
/>
<input
v-b-tooltip.hover
type="color"
class="form-control form-control-color"
:value="
colorConvert(
editor.getAttributes('textStyle').backgroundColor,
'#000000'
)
"
@input="
editor.chain().focus().setBackgroundColor(($event.target as HTMLInputElement).value).run()
"
v-b-tooltip.hover
:value="colorConvert(editor.getAttributes('textStyle').backgroundColor, '#000000')"
title="배경색"
@input="
editor?.chain().focus().setBackgroundColor(($event.target as HTMLInputElement).value).run()
"
/>
</b-button-group>
</BButtonGroup>
<b-button-group class="mx-1">
<b-button
v-b-tooltip.hover
@click="showImageModal = true"
title="이미지 추가"
><i class="bi bi-image"></i
></b-button>
<BButtonGroup class="mx-1">
<BButton v-b-tooltip.hover title="이미지 추가" @click="showImageModal = true">
<i class="bi bi-image" />
</BButton>
<!-- 이미지추가 -->
<!-- 링크 -->
<!-- 영상링크 -->
<!-- -->
<!-- 구분선 삽입 -->
<b-button
@click="editor.chain().focus().setHorizontalRule().run()"
v-b-tooltip.hover
title="구분선"
><i class="bi bi-hr"></i
></b-button>
</b-button-group>
<BButton v-b-tooltip.hover title="구분선" @click="editor?.chain().focus().setHorizontalRule().run()">
<i class="bi bi-hr" />
</BButton>
</BButtonGroup>
<b-button-group class="mx-1">
<BButtonGroup class="mx-1">
<!-- 글머리 기호 -->
<!-- 번호 매기기 -->
<b-button
@click="editor.chain().focus().setTextAlign('left').run()"
<BButton
v-b-tooltip.hover
:class="{ 'is-active': editor.isActive({ textAlign: 'left' }) }"
v-b-tooltip.hover
title="왼쪽 정렬"
><i class="bi bi-text-left"></i
></b-button>
<b-button
@click="editor.chain().focus().setTextAlign('center').run()"
@click="editor?.chain().focus().setTextAlign('left').run()"
>
<i class="bi bi-text-left" />
</BButton>
<BButton
v-b-tooltip.hover
:class="{ 'is-active': editor.isActive({ textAlign: 'center' }) }"
v-b-tooltip.hover
title="가운데 정렬"
><i class="bi bi-text-center"></i
></b-button>
<b-button
@click="editor.chain().focus().setTextAlign('right').run()"
:class="{ 'is-active': editor.isActive({ textAlign: 'right' }) }"
@click="editor?.chain().focus().setTextAlign('center').run()"
>
<i class="bi bi-text-center" />
</BButton>
<BButton
v-b-tooltip.hover
:class="{ 'is-active': editor.isActive({ textAlign: 'right' }) }"
title="오른쪽 정렬"
><i class="bi bi-text-right"></i
></b-button>
@click="editor?.chain().focus().setTextAlign('right').run()"
>
<i class="bi bi-text-right" />
</BButton>
<!-- 문단정렬(, , , )(내어, 들여) -->
</b-button-group>
</BButtonGroup>
<b-button-group class="mx-1"> </b-button-group>
<BButtonGroup class="mx-1" />
<b-button-group class="mx-1">
<BButtonGroup class="mx-1">
<!-- 줄간격 (1.0, 1.2, 1.4, 1.5, 1.6, 1.8, 2.0, 3.0) -->
</b-button-group>
</BButtonGroup>
<b-button-group class="mx-1">
<BButtonGroup class="mx-1">
<!-- 원본 코드 -->
</b-button-group>
</b-button-toolbar>
<bubble-menu
:tippy-options="{ animation: false, maxWidth: 600 }"
:editor="editor"
</BButtonGroup>
</BButtonToolbar>
<BubbleMenu
v-if="editable && editor"
v-show="editor.isActive('custom-image')"
:tippyOptions="{ animation: false, maxWidth: 600 }"
:editor="editor"
>
<b-button-toolbar>
<b-button-group class="mx-1">
<b-button
@click="editor.chain().focus().setImageEx({ size: 'small' }).run()"
<BButtonToolbar>
<BButtonGroup class="mx-1">
<BButton
v-b-tooltip.hover
:class="{
'is-active': editor.isActive('custom-image', {
size: 'small',
}),
f_frac: true,
}"
v-b-tooltip.hover
title="1/4 너비로 채우기"
>1/4</b-button
@click="editor?.chain().focus().setImageEx({ size: 'small' }).run()"
>
<b-button
@click="editor.chain().focus().setImageEx({ size: 'medium' }).run()"
1/4
</BButton>
<BButton
v-b-tooltip.hover
:class="{
'is-active': editor.isActive('custom-image', {
size: 'medium',
}),
f_frac: true,
}"
v-b-tooltip.hover
title="1/2 너비로 채우기"
>1/2</b-button
@click="editor?.chain().focus().setImageEx({ size: 'medium' }).run()"
>
<b-button
@click="editor.chain().focus().setImageEx({ size: 'large' }).run()"
1/2
</BButton>
<BButton
v-b-tooltip.hover
:class="{
'is-active': editor.isActive('custom-image', {
size: 'large',
}),
f_frac: true,
}"
v-b-tooltip.hover
title="가득 채우기"
>1</b-button
@click="editor?.chain().focus().setImageEx({ size: 'large' }).run()"
>
<b-button
@click="editor.chain().focus().setImageEx({ size: 'original' }).run()"
1
</BButton>
<BButton
:class="{
'is-active': editor.isActive('custom-image', {
size: 'original',
}),
}"
>원본</b-button
@click="editor?.chain().focus().setImageEx({ size: 'original' }).run()"
>
</b-button-group>
<b-button-group class="mx-1">
<b-button
@click="
editor.chain().focus().setImageEx({ align: 'float-left' }).run()
"
원본
</BButton>
</BButtonGroup>
<BButtonGroup class="mx-1">
<BButton
v-b-tooltip.hover
:class="{
'is-active': editor.isActive('custom-image', {
float: 'float-left',
}),
}"
v-b-tooltip.hover
title="왼쪽으로 붙이기"
><i class="bi bi-chevron-bar-left"></i
></b-button>
<b-button
@click="editor.chain().focus().setImageEx({ align: 'left' }).run()"
@click="editor?.chain().focus().setImageEx({ align: 'float-left' }).run()"
>
<i class="bi bi-chevron-bar-left" />
</BButton>
<BButton
v-b-tooltip.hover
:class="{
'is-active': editor.isActive('custom-image', {
float: 'left',
}),
}"
v-b-tooltip.hover
title="왼쪽으로"
><i class="bi bi-align-start"></i
></b-button>
<b-button
@click="editor.chain().focus().setImageEx({ align: 'center' }).run()"
@click="editor?.chain().focus().setImageEx({ align: 'left' }).run()"
>
<i class="bi bi-align-start" />
</BButton>
<BButton
v-b-tooltip.hover
:class="{
'is-active': editor.isActive('custom-image', {
float: 'center',
}),
}"
v-b-tooltip.hover
title="가운데로"
><i class="bi bi-align-center"></i
></b-button>
<b-button
@click="editor.chain().focus().setImageEx({ align: 'right' }).run()"
@click="editor?.chain().focus().setImageEx({ align: 'center' }).run()"
>
<i class="bi bi-align-center" />
</BButton>
<BButton
v-b-tooltip.hover
:class="{
'is-active': editor.isActive('custom-image', {
float: 'right',
}),
}"
v-b-tooltip.hover
title="오른쪽으로 붙이기"
><i class="bi bi-align-end"></i
></b-button>
<b-button
@click="
editor.chain().focus().setImageEx({ align: 'float-right' }).run()
"
@click="editor?.chain().focus().setImageEx({ align: 'right' }).run()"
>
<i class="bi bi-align-end" />
</BButton>
<BButton
v-b-tooltip.hover
:class="{
'is-active': editor.isActive('custom-image', {
float: 'float-right',
}),
}"
v-b-tooltip.hover
title="오른쪽으로 붙이기"
><i class="bi bi-chevron-bar-right"></i
></b-button>
</b-button-group>
</b-button-toolbar>
</bubble-menu>
<editor-content :editor="editor" class="tiptap-editor" />
<b-modal
@click="editor?.chain().focus().setImageEx({ align: 'float-right' }).run()"
>
<i class="bi bi-chevron-bar-right" />
</BButton>
</BButtonGroup>
</BButtonToolbar>
</BubbleMenu>
<EditorContent :editor="editor" class="tiptap-editor" />
<BModal
v-model="showImageModal"
title="이미지 추가"
okTitle="추가"
@@ -295,7 +291,7 @@
@hidden="resetModal"
>
<div class="bg-light text-dark">
<b-form-group
<BFormGroup
label-cols-sm="4"
label-cols-lg="3"
content-cols-sm
@@ -306,14 +302,14 @@
:label-for="`${uuid}_image_upload`"
>
<input
:id="`${uuid}_image_upload`"
class="form-control"
type="file"
:id="`${uuid}_image_upload`"
@change="chooseImage"
accept=".jpg,.jpeg,.png,.gif,.webp"
@change="chooseImage"
/>
</b-form-group>
<b-form-group
</BFormGroup>
<BFormGroup
label-cols-sm="4"
label-cols-lg="3"
content-cols-sm
@@ -323,15 +319,15 @@
label-align="right"
:label-for="`${uuid}_image_link`"
>
<b-form-input v-model="imageLink"></b-form-input>
</b-form-group>
<BFormInput v-model="imageLink" />
</BFormGroup>
</div>
</b-modal>
</BModal>
</template>
<script lang="ts">
<script lang="ts" setup>
//import "@scss/common/bootstrap5.scss";
import { defineComponent } from "vue";
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
import { Editor, EditorContent, BubbleMenu } from "@tiptap/vue-3";
import { FontSize } from "@/tiptap-ext/FontSize";
import StarterKit from "@tiptap/starter-kit";
@@ -351,6 +347,8 @@ import {
BDropdownItem,
BDropdownDivider,
BModal,
BFormGroup,
BFormInput,
} from "bootstrap-vue-3";
import { v4 as uuidv4 } from "uuid";
import { unwrap } from "@/util/unwrap";
@@ -359,178 +357,160 @@ import { isObject, isString } from "lodash";
import type { AxiosError } from "axios";
import { SammoAPI } from "@/SammoAPI";
const compoment = defineComponent({
components: {
EditorContent,
BubbleMenu,
BModal,
BButtonGroup,
BButtonToolbar,
BButton,
BDropdown,
BDropdownItem,
BDropdownDivider,
const props = defineProps({
modelValue: {
type: String,
default: "",
},
emits: ["ready", "update:modelValue"],
methods: {
unwrap,
chooseImage(e: Event) {
const target = unwrap(e.target) as HTMLInputElement;
this.imageUploadFiles = target.files;
},
colorConvert(val: string | undefined, defaultVal: string) {
if (!val) {
return defaultVal;
}
if (val.startsWith("rgb")) {
const rgb = val.split("(")[1].split(")")[0].split(",");
const vals: string[] = [];
for (const subColor of rgb) {
const hexSubColor = parseInt(subColor).toString(16);
if (hexSubColor.length == 1) {
vals.push("0");
}
vals.push(hexSubColor);
}
return `#${vals.join("")}`;
}
return val;
},
async tryAddImage(bvModalEvt: Event) {
if (this.imageUploadFiles === null || this.imageUploadFiles.length == 0) {
this.addImageLink(bvModalEvt);
return;
}
const targetImage = unwrap(this.imageUploadFiles.item(0));
let imageResult: {
result: true;
path: string;
};
try {
const base64Binary = await getBase64FromFileObject(targetImage);
imageResult = await SammoAPI.Misc.UploadImage({
imageData: base64Binary,
});
} catch (e) {
if (isString(e)) {
alert(e);
bvModalEvt.preventDefault();
}
if (isObject(e) && "response" in e) {
const axiosErr = e as AxiosError;
if (axiosErr.response?.status === 413) {
alert("허용 용량을 초과했습니다.");
bvModalEvt.preventDefault();
}
}
console.error(e);
return false;
}
const imagePath = imageResult.path;
this.editor.chain().focus().setImageEx({ src: imagePath }).run();
},
addImageLink(bvModalEvt: Event) {
if (!this.imageLink) {
alert("업로드할 이미지를 선택하거나, 이미지 주소를 입력해주세요.");
bvModalEvt.preventDefault();
return false;
}
this.editor.chain().focus().setImageEx({ src: this.imageLink }).run();
},
resetModal() {
this.imageLink = "";
this.imageUploadFiles = null;
},
},
props: {
modelValue: {
type: String,
default: "",
},
editable: {
type: Boolean,
default: true,
},
},
data() {
return {
uuid: uuidv4(),
editor: null as unknown as InstanceType<typeof Editor>,
fontList: ["Pretendard", "맑은 고딕", "궁서", "돋움"],
fontSize: [
"8px",
"10px",
"12px",
"14px",
"18px",
"22px",
"28px",
"36px",
"48px",
"72px",
],
imageUploadFiles: null as FileList | null,
imageLink: "",
showImageModal: false,
};
},
watch: {
modelValue(value: string) {
const isSame = this.editor.getHTML() === value;
if (isSame) {
return;
}
this.editor.commands.setContent(value, false);
},
editable(value: boolean) {
this.editor.options.editable = value;
if (value == true) {
this.editor.commands.focus();
}
},
},
mounted() {
const editor = new Editor({
extensions: [
StarterKit,
Underline,
FontSize,
TextStyle,
TextAlign.configure({
types: ["heading", "paragraph"],
}),
Color.configure({
types: ["textStyle"],
}),
BackgroundColor.configure({
types: ["textStyle"],
}),
CustomImage,
Link,
],
editable: this.editable,
content: this.modelValue,
onUpdate: () => {
this.$emit("update:modelValue", this.editor.getHTML());
},
onCreate: () => {
this.$emit("ready");
}
});
this.editor = editor;
},
beforeUnmount() {
this.editor.destroy();
editable: {
type: Boolean,
default: true,
},
});
export default compoment;
</script>
const emit = defineEmits(["ready", "update:modelValue"]);
const uuid = ref(uuidv4());
const editor = ref<InstanceType<typeof Editor>>();
//const fontList = ref(["Pretendard", "맑은 고딕", "궁서", "돋움"]);
const fontSize = ref(["8px", "10px", "12px", "14px", "18px", "22px", "28px", "36px", "48px", "72px"]);
const imageUploadFiles = ref(null as FileList | null);
const imageLink = ref("");
const showImageModal = ref(false);
watch(
() => props.modelValue,
(value: string) => {
const isSame = editor.value?.getHTML() === value;
if (isSame) {
return;
}
editor.value?.commands.setContent(value, false);
}
);
watch(
() => props.editable,
(value: boolean) => {
if (!editor.value) {
return;
}
editor.value.options.editable = value;
if (value == true) {
editor.value.commands.focus();
}
}
);
onMounted(() => {
const vEditor = new Editor({
extensions: [
StarterKit,
Underline,
FontSize,
TextStyle,
TextAlign.configure({
types: ["heading", "paragraph"],
}),
Color.configure({
types: ["textStyle"],
}),
BackgroundColor.configure({
types: ["textStyle"],
}),
CustomImage,
Link,
],
editable: props.editable,
content: props.modelValue,
onUpdate: () => {
emit("update:modelValue", editor.value?.getHTML());
},
onCreate: () => {
emit("ready");
},
});
editor.value = vEditor;
});
onBeforeUnmount(() => {
editor.value?.destroy();
});
function chooseImage(e: Event) {
const target = unwrap(e.target) as HTMLInputElement;
imageUploadFiles.value = target.files;
}
function colorConvert(val: string | undefined, defaultVal: string) {
if (!val) {
return defaultVal;
}
if (val.startsWith("rgb")) {
const rgb = val.split("(")[1].split(")")[0].split(",");
const vals: string[] = [];
for (const subColor of rgb) {
const hexSubColor = parseInt(subColor).toString(16);
if (hexSubColor.length == 1) {
vals.push("0");
}
vals.push(hexSubColor);
}
return `#${vals.join("")}`;
}
return val;
}
async function tryAddImage(bvModalEvt: Event) {
if (imageUploadFiles.value === null || imageUploadFiles.value.length == 0) {
addImageLink(bvModalEvt);
return;
}
const targetImage = unwrap(unwrap(imageUploadFiles.value).item(0));
let imageResult: {
result: true;
path: string;
};
try {
const base64Binary = await getBase64FromFileObject(targetImage);
imageResult = await SammoAPI.Misc.UploadImage({
imageData: base64Binary,
});
} catch (e) {
if (isString(e)) {
alert(e);
bvModalEvt.preventDefault();
}
if (isObject(e) && "response" in e) {
const axiosErr = e as AxiosError;
if (axiosErr.response?.status === 413) {
alert("허용 용량을 초과했습니다.");
bvModalEvt.preventDefault();
}
}
console.error(e);
return false;
}
const imagePath = imageResult.path;
editor.value?.chain().focus().setImageEx({ src: imagePath }).run();
}
function addImageLink(bvModalEvt: Event) {
if (!imageLink.value) {
alert("업로드할 이미지를 선택하거나, 이미지 주소를 입력해주세요.");
bvModalEvt.preventDefault();
return false;
}
editor.value?.chain().focus().setImageEx({ src: imageLink.value }).run();
}
function resetModal() {
imageLink.value = "";
imageUploadFiles.value = null;
}
</script>
+49 -64
View File
@@ -1,85 +1,70 @@
<template>
<div class="bg0 back_bar">
<button type="button" class="btn btn-sammo-base2 back_btn" @click="back">
돌아가기</button
><button
type="button"
v-if="reloadable"
class="btn btn-sammo-base2 reload_btn"
@click="reload"
>
갱신
</button>
<div v-else></div>
<h2 class="title">{{ title }}</h2>
<button type="button" class="btn btn-sammo-base2 back_btn" @click="back">돌아가기</button
><button v-if="reloadable" type="button" class="btn btn-sammo-base2 reload_btn" @click="reload">갱신</button>
<div v-else />
<h2 class="title">
{{ title }}
</h2>
<div>&nbsp;</div>
<b-button
v-if="toggleSearch !== undefined"
class="btn-toggle-zoom"
:variant="toggleSearch ? 'info' : 'secondary'"
:pressed="toggleSearch"
v-if="toggleSearch !== undefined"
@click="toggleSearch = !toggleSearch"
>{{ toggleSearch ? "검색 켜짐" : "검색 꺼짐" }}</b-button
>
{{ toggleSearch ? "검색 켜짐" : "검색 꺼짐" }}
</b-button>
</div>
</template>
<script lang="ts">
<script lang="ts" setup>
import "@scss/game_bg.scss";
import { defineComponent, type PropType } from "vue";
import { type PropType, ref, watch } from "vue";
import VueTypes from "vue-types";
export default defineComponent({
name: "TopBackBar",
methods: {
back() {
if (this.type === "normal") {
location.href = "./";
} else if (this.type == "chief") {
location.href = "v_chiefCenter.php";
} else {
//TODO: window.close하려면 부모창이 있어야함!
window.close();
}
},
reload() {
this.$emit("reload");
},
const props = defineProps({
title: VueTypes.string.isRequired,
type: {
type: String as PropType<"normal" | "chief" | "close">,
default: "normal",
required: false,
},
data() {
return {
toggleSearch: this.searchable,
};
searchable: {
type: Boolean,
default: undefined,
required: false,
},
emits: ["update:searchable", "reload"],
watch: {
toggleSearch(val: boolean) {
this.$emit("update:searchable", val);
},
},
props: {
title: {
type: String,
required: true,
},
type: {
type: String as PropType<"normal" | "chief" | "close">,
default: "normal",
required: false,
},
searchable: {
type: Boolean,
default: undefined,
required: false,
},
reloadable: {
type: Boolean,
default: undefined,
required: false,
},
reloadable: {
type: Boolean,
default: undefined,
required: false,
},
});
</script>
const emit = defineEmits(["update:searchable", "reload"]);
const toggleSearch = ref(props.searchable);
watch(toggleSearch, (val) => {
emit("update:searchable", val);
});
function back() {
if (props.type === "normal") {
location.href = "./";
} else if (props.type == "chief") {
location.href = "v_chiefCenter.php";
} else {
//TODO: window.close하려면 부모창이 있어야함!
window.close();
}
}
function reload() {
emit("reload");
}
</script>
<style scoped>
.back_bar {
@@ -112,4 +97,4 @@ export default defineComponent({
font-size: 18pt;
margin: 0;
}
</style>
</style>