feat: 새 설문조사 모듈 UI
This commit is contained in:
@@ -7,6 +7,7 @@ use sammo\DB;
|
|||||||
use sammo\DTO\VoteComment;
|
use sammo\DTO\VoteComment;
|
||||||
use sammo\General;
|
use sammo\General;
|
||||||
use sammo\Session;
|
use sammo\Session;
|
||||||
|
use sammo\TimeUtil;
|
||||||
use sammo\Validator;
|
use sammo\Validator;
|
||||||
|
|
||||||
class AddComment extends \sammo\BaseAPI
|
class AddComment extends \sammo\BaseAPI
|
||||||
@@ -23,7 +24,7 @@ class AddComment extends \sammo\BaseAPI
|
|||||||
'voteID',
|
'voteID',
|
||||||
'text',
|
'text',
|
||||||
])->rule('lengthMin', 'text', 1)
|
])->rule('lengthMin', 'text', 1)
|
||||||
->rule('int', 'multipleOptions');
|
->rule('int', 'voteID');
|
||||||
|
|
||||||
if (!$v->validate()) {
|
if (!$v->validate()) {
|
||||||
return $v->errorStr();
|
return $v->errorStr();
|
||||||
@@ -34,21 +35,24 @@ class AddComment extends \sammo\BaseAPI
|
|||||||
function launch(Session $session, ?DateTimeInterface $modifiedSince, ?string $reqEtag)
|
function launch(Session $session, ?DateTimeInterface $modifiedSince, ?string $reqEtag)
|
||||||
{
|
{
|
||||||
$voteID = $this->args['voteID'];
|
$voteID = $this->args['voteID'];
|
||||||
$text = $this->args['text'];
|
$text = mb_substr($this->args['text'], 0, 200);
|
||||||
|
|
||||||
$generalID = $session->generalID;
|
$generalID = $session->generalID;
|
||||||
$general = General::createGeneralObjFromDB($generalID, [], 0);
|
$general = General::createGeneralObjFromDB($generalID, [], 0);
|
||||||
$generalName = $general->getName();
|
$generalName = $general->getName();
|
||||||
$nationID = $general->getNationID();
|
$nationID = $general->getNationID();
|
||||||
$nationName = $general->getStaticNation()['name'];
|
$nationName = $general->getStaticNation()['name'];
|
||||||
|
$date = TimeUtil::now();
|
||||||
|
|
||||||
|
|
||||||
$comment = new VoteComment(
|
$comment = new VoteComment(
|
||||||
voteID: $voteID,
|
vote_id: $voteID,
|
||||||
generalID: $generalID,
|
general_id: $generalID,
|
||||||
nationID: $nationID,
|
nation_id: $nationID,
|
||||||
nationName: $nationName,
|
nation_name: $nationName,
|
||||||
generalName: $generalName,
|
general_name: $generalName,
|
||||||
text: $text,
|
text: $text,
|
||||||
|
date: $date
|
||||||
);
|
);
|
||||||
|
|
||||||
$db = DB::db();
|
$db = DB::db();
|
||||||
|
|||||||
@@ -44,17 +44,31 @@ class GetVoteDetail extends \sammo\BaseAPI
|
|||||||
|
|
||||||
|
|
||||||
$votes = array_map(fn ($arr) => [Json::decode($arr[0]), $arr[1]], $db->queryAllLists(
|
$votes = array_map(fn ($arr) => [Json::decode($arr[0]), $arr[1]], $db->queryAllLists(
|
||||||
'SELECT selection, count(*) AS cnt FROM vote_result WHERE voteID = %i GROUP BY selection',
|
'SELECT selection, count(*) AS cnt FROM vote WHERE vote_id = %i GROUP BY selection',
|
||||||
$voteID
|
$voteID
|
||||||
));
|
));
|
||||||
|
|
||||||
$comments = VoteComment::arrayOf($db->query('SELECT * FROM vote_comment WHERE voteID = %i ORDER BY `id` ASC', $voteID));
|
$comments = VoteComment::arrayOf($db->query('SELECT * FROM vote_comment WHERE vote_id = %i ORDER BY `id` ASC', $voteID));
|
||||||
|
|
||||||
|
$myVote = null;
|
||||||
|
if($session->isGameLoggedIn()){
|
||||||
|
$generalID = $session->generalID;
|
||||||
|
$rawMyVote = $db->queryFirstField('SELECT selection FROM vote WHERE vote_id = %i AND general_id = %i', $voteID, $generalID);
|
||||||
|
if($rawMyVote){
|
||||||
|
$myVote = Json::decode($rawMyVote);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$userCnt = $db->queryFirstField('SELECT count(*) FROM general WHERE npc < 2');
|
||||||
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'result' => true,
|
'result' => true,
|
||||||
'voteInfo' => $voteInfo,
|
'voteInfo' => $voteInfo,
|
||||||
'votes' => $votes,
|
'votes' => $votes,
|
||||||
'comments' => $comments
|
'comments' => $comments,
|
||||||
|
'myVote' => $myVote,
|
||||||
|
'userCnt' => $userCnt,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use sammo\DB;
|
|||||||
use sammo\DTO\VoteInfo;
|
use sammo\DTO\VoteInfo;
|
||||||
use sammo\KVStorage;
|
use sammo\KVStorage;
|
||||||
use sammo\Session;
|
use sammo\Session;
|
||||||
|
use sammo\TimeUtil;
|
||||||
use sammo\Util;
|
use sammo\Util;
|
||||||
use sammo\Validator;
|
use sammo\Validator;
|
||||||
|
|
||||||
@@ -47,7 +48,7 @@ class NewVote extends \sammo\BaseAPI
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$lastVoteInfo->endDate = new \DateTimeImmutable();;
|
$lastVoteInfo->endDate = TimeUtil::now();
|
||||||
$voteStor->setValue("vote_{$voteID}", $lastVoteInfo->toArray());
|
$voteStor->setValue("vote_{$voteID}", $lastVoteInfo->toArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,12 +69,29 @@ class NewVote extends \sammo\BaseAPI
|
|||||||
$multipleOptions = 0;
|
$multipleOptions = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$now = TimeUtil::now();
|
||||||
$now = new \DateTimeImmutable();
|
/** @var ?string */
|
||||||
$endDate = $this->args['endDate'] ?? null;
|
$endDate = $this->args['endDate'] ?? null;
|
||||||
/** @var string[] */
|
/** @var string[] */
|
||||||
$options = $this->args['options'] ?? [];
|
$options = $this->args['options'] ?? [];
|
||||||
|
|
||||||
|
if(!$options){
|
||||||
|
return '항목이 없습니다.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if($endDate !== null){
|
||||||
|
try{
|
||||||
|
$oNow = new \DateTimeImmutable($now);
|
||||||
|
$oEndDate = new \DateTimeImmutable($endDate);
|
||||||
|
if($oEndDate < $oNow){
|
||||||
|
return '종료일이 이미 지났습니다.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(\Throwable $e){
|
||||||
|
return '종료일이 잘못되었습니다.'.$e->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$db = DB::db();
|
$db = DB::db();
|
||||||
$gameStor = KVStorage::getStorage($db, 'game_env');
|
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||||
|
|
||||||
@@ -99,6 +117,8 @@ class NewVote extends \sammo\BaseAPI
|
|||||||
);
|
);
|
||||||
|
|
||||||
$voteStor->setValue("vote_{$voteID}", $voteInfo->toArray());
|
$voteStor->setValue("vote_{$voteID}", $voteInfo->toArray());
|
||||||
|
$gameStor->setValue('lastVote', $voteID);
|
||||||
|
|
||||||
$db->update('general', [
|
$db->update('general', [
|
||||||
'newvote' => 1
|
'newvote' => 1
|
||||||
], true);
|
], true);
|
||||||
|
|||||||
@@ -33,4 +33,6 @@ class VoteComment extends DataTransferObject
|
|||||||
public string $generalName;
|
public string $generalName;
|
||||||
|
|
||||||
public string $text;
|
public string $text;
|
||||||
|
|
||||||
|
public string $date;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,16 +9,15 @@ use Spatie\DataTransferObject\Casters\ArrayCaster;
|
|||||||
|
|
||||||
//https://json2dto.atymic.dev/
|
//https://json2dto.atymic.dev/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#[Strict]
|
#[Strict]
|
||||||
class VoteInfo extends DataTransferObject
|
class VoteInfo extends DataTransferObject
|
||||||
{
|
{
|
||||||
public int $id;
|
public int $id;
|
||||||
public string $title;
|
public string $title;
|
||||||
public int $multipleOptions;
|
public int $multipleOptions;
|
||||||
public \DateTimeImmutable $startDate;
|
|
||||||
public ?\DateTimeImmutable $endDate;
|
public string $startDate;
|
||||||
|
public ?string $endDate;
|
||||||
|
|
||||||
/** @var string[] */
|
/** @var string[] */
|
||||||
public array $options;
|
public array $options;
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
@import '@scss/common/bootstrap5.scss';
|
||||||
|
@import "@scss/game_bg.scss";
|
||||||
|
@import "@scss/util.scss";
|
||||||
|
|
||||||
|
@include media-1000px{
|
||||||
|
#container {
|
||||||
|
width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@include media-500px{
|
||||||
|
body {
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#container {
|
||||||
|
width: 500px;
|
||||||
|
margin: 0 auto;
|
||||||
|
margin-bottom: 100px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -682,6 +682,7 @@ CREATE TABLE `vote_comment` (
|
|||||||
`general_name` VARCHAR(32) NOT NULL COLLATE 'utf8mb4_bin',
|
`general_name` VARCHAR(32) NOT NULL COLLATE 'utf8mb4_bin',
|
||||||
`nation_name` VARCHAR(64) NOT NULL COLLATE 'utf8mb4_bin',
|
`nation_name` VARCHAR(64) NOT NULL COLLATE 'utf8mb4_bin',
|
||||||
`text` TEXT NOT NULL,
|
`text` TEXT NOT NULL,
|
||||||
|
`date` DATETIME NULL DEFAULT NULL,
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
INDEX `by_vote` (`vote_id`)
|
INDEX `by_vote` (`vote_id`)
|
||||||
) COLLATE = 'utf8mb4_general_ci' ENGINE = Aria;
|
) COLLATE = 'utf8mb4_general_ci' ENGINE = Aria;
|
||||||
@@ -0,0 +1,502 @@
|
|||||||
|
<template>
|
||||||
|
<BContainer id="container" :toast="{ root: true }" class="pageVote bg0">
|
||||||
|
<TopBackBar :reloadable="true" title="" @reload="reloadVote" />
|
||||||
|
<div id="vote-title" class="bg2">설문 조사({{ voteReward }}금과 추첨으로 유니크템 증정!</div>
|
||||||
|
<table v-if="currentVote" id="vote-result">
|
||||||
|
<colgroup>
|
||||||
|
<col class="vote-idx" />
|
||||||
|
<col class="vote-count" />
|
||||||
|
<col class="vote-percent" />
|
||||||
|
<col class="vote-option" />
|
||||||
|
</colgroup>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th colspan="3" class="text-end bg1">설문 제목</th>
|
||||||
|
<th id="vote-detail-title">{{ currentVote.voteInfo.title }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(option, idx) in currentVote.voteInfo.options" :key="idx">
|
||||||
|
<template v-if="canVote">
|
||||||
|
<td v-if="currentVote.voteInfo.multipleOptions == 1" class="text-center">
|
||||||
|
<input :id="`v-vote-${idx}`" v-model="mySinglePick" class="form-check-input" type="radio" :value="idx" />
|
||||||
|
</td>
|
||||||
|
<td v-else class="text-center">
|
||||||
|
<input
|
||||||
|
:id="`v-vote-${idx}`"
|
||||||
|
v-model="myMultiPick"
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
:value="idx"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</template>
|
||||||
|
<td
|
||||||
|
v-else
|
||||||
|
class="text-end f_tnum"
|
||||||
|
:style="{
|
||||||
|
backgroundColor: formatVoteColor(idx),
|
||||||
|
color: isBrightColor(formatVoteColor(idx)) ? '#000' : '#fff',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ idx + 1 }}.
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="text-end f_tnum vote-count">
|
||||||
|
<label :for="`v-vote-${idx}`">{{ voteDistribution[idx] }}명</label>
|
||||||
|
</td>
|
||||||
|
<td class="text-end f_tnum vote-percent">
|
||||||
|
<label :for="`v-vote-${idx}`"
|
||||||
|
>({{ ((voteDistribution[idx] / Math.max(1, voteTotal)) * 100).toFixed(1) }}%)</label
|
||||||
|
>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<label :for="`v-vote-${idx}`">{{ option }}</label>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<template v-if="canVote">
|
||||||
|
<td class="text-center">투표</td>
|
||||||
|
<td colspan="2">
|
||||||
|
<div class="d-grid"><BButton @click="submitVote">투표</BButton></div>
|
||||||
|
</td>
|
||||||
|
</template>
|
||||||
|
<td v-else colspan="3" class="text-center">결산</td>
|
||||||
|
<td>
|
||||||
|
투표율: {{ voteTotal }} / {{ currentVote.userCnt }} (
|
||||||
|
{{ ((voteTotal / Math.max(1, currentVote.userCnt)) * 100).toFixed(1) }}%)
|
||||||
|
</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
<form @submit.prevent="submitComment">
|
||||||
|
<table v-if="currentVote" id="vote-comment">
|
||||||
|
<colgroup>
|
||||||
|
<col class="comment-idx" />
|
||||||
|
<col class="comment-name" />
|
||||||
|
<col class="comment-text" />
|
||||||
|
<col class="comment-date" />
|
||||||
|
</colgroup>
|
||||||
|
<thead>
|
||||||
|
<tr class="bg1 text-center">
|
||||||
|
<th>#</th>
|
||||||
|
<th>
|
||||||
|
<div class="row gx-0">
|
||||||
|
<div class="col-12 col-md-6">국가명</div>
|
||||||
|
<div class="col-12 col-md-6">장수명</div>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
<th>댓글</th>
|
||||||
|
<th>일시</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(comment, idx) in currentVote.comments" :key="idx">
|
||||||
|
<td class="comment-idx f_tnum">{{ idx + 1 }}.</td>
|
||||||
|
<td class="comment-name">
|
||||||
|
<div class="row gx-0">
|
||||||
|
<div class="col-12 col-md-6">{{ comment.nationName }}</div>
|
||||||
|
<div class="col-12 col-md-6">{{ comment.generalName }}</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>{{ comment.text }}</td>
|
||||||
|
<td class="comment-date f_tnum">{{ comment.date.substring(5, 5 + 5 + 1 + 5) }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td>
|
||||||
|
<div class="offset-md-6 d-grid"><BButton type="submit">댓글 달기</BButton></div>
|
||||||
|
</td>
|
||||||
|
<td colspan="2"><BFormInput v-model="myComment" /></td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</form>
|
||||||
|
<div id="vote-old-title" class="bg2">이전 설문 조사</div>
|
||||||
|
<div id="vote-old-list">
|
||||||
|
<div v-for="[voteID, voteInfo] of voteList" :key="voteID" class="vote-old-item">
|
||||||
|
<div class="row">
|
||||||
|
<a href="#" @click.prevent="currentVoteID = voteID">{{ voteInfo.title }}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="isVoteAdmin" id="vote-new-panel">
|
||||||
|
<div class="row"><a href="#" @click.prevent="showNewVote = !showNewVote">새 설문 조사 열기</a></div>
|
||||||
|
<template v-if="showNewVote">
|
||||||
|
<div class="row gx-0">
|
||||||
|
<div class="col-md-3">설문 제목</div>
|
||||||
|
<div class="col-md-9"><BFormInput v-model="newVoteInfo.title" type="text" /></div>
|
||||||
|
</div>
|
||||||
|
<div class="row gx-0">
|
||||||
|
<div class="col-md-3">설문 대상(엔터로 구분) ({{ newVoteInfo.options.length }}건)</div>
|
||||||
|
<div class="col-md-9">
|
||||||
|
<textarea v-model="newVoteOptionsText" class="form-control" :rows="newVoteOptionsLength + 1"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row gx-0">
|
||||||
|
<div class="col-md-3">동시 응답 수(0=모두)</div>
|
||||||
|
<div class="col-md-9">
|
||||||
|
<BFormInput
|
||||||
|
v-model="newVoteInfo.multipleOptions"
|
||||||
|
type="number"
|
||||||
|
:min="0"
|
||||||
|
:max="newVoteInfo.options.length"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row gx-0">
|
||||||
|
<div class="offset-8 col-4 offset-md-10 col-md-2 d-grid">
|
||||||
|
<BButton @click="submitNewVote">제출</BButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<BottomBar type="close" />
|
||||||
|
</BContainer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
declare const staticValues: {
|
||||||
|
serverNick: string;
|
||||||
|
serverID: string;
|
||||||
|
isGameLoggedIn: boolean;
|
||||||
|
isVoteAdmin: boolean;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import TopBackBar from "@/components/TopBackBar.vue";
|
||||||
|
import BottomBar from "@/components/BottomBar.vue";
|
||||||
|
import { BContainer, useToast, BButton, BFormInput } from "bootstrap-vue-3";
|
||||||
|
import { unwrap } from "@/util/unwrap";
|
||||||
|
import { onMounted, reactive, ref, watch, computed } from "vue";
|
||||||
|
import type { VoteInfo, VoteDetailResult } from "@/defs/API/Vote";
|
||||||
|
import { SammoAPI } from "@/SammoAPI";
|
||||||
|
import { isString, range, sum } from "lodash";
|
||||||
|
import { formatTime } from "@/util/formatTime";
|
||||||
|
import { isBrightColor } from "@/util/isBrightColor";
|
||||||
|
import { formatVoteColor } from "@/utilGame/formatVoteColor";
|
||||||
|
|
||||||
|
const { isVoteAdmin, isGameLoggedIn } = staticValues;
|
||||||
|
const voteList = ref(new Map<number, VoteInfo>());
|
||||||
|
|
||||||
|
const voteReward = 10;
|
||||||
|
const toasts = unwrap(useToast());
|
||||||
|
|
||||||
|
const showNewVote = ref(false);
|
||||||
|
|
||||||
|
const voteTotal = ref(0);
|
||||||
|
const voteDistribution = ref<Record<number, number>>({});
|
||||||
|
|
||||||
|
const currentVote = ref<VoteDetailResult>();
|
||||||
|
watch(currentVote, (voteResult) => {
|
||||||
|
if (!voteResult) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const voteDist: Record<number, number> = {};
|
||||||
|
for (const idx of range(voteResult.voteInfo.options.length)) {
|
||||||
|
voteDist[idx] = 0;
|
||||||
|
}
|
||||||
|
voteTotal.value = sum(voteResult.votes.map(([, count]) => count));
|
||||||
|
|
||||||
|
for (const [selection, count] of voteResult.votes) {
|
||||||
|
for (const idx of selection) {
|
||||||
|
voteDist[idx] += count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
voteDistribution.value = voteDist;
|
||||||
|
});
|
||||||
|
|
||||||
|
const canVote = computed(() => {
|
||||||
|
if (!isGameLoggedIn) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!currentVote.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (currentVote.value.myVote) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const endDate = currentVote.value.voteInfo.endDate;
|
||||||
|
if (endDate) {
|
||||||
|
const now = formatTime(new Date());
|
||||||
|
if (now > endDate) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentVoteID = ref<number>();
|
||||||
|
watch(currentVoteID, async (voteID) => {
|
||||||
|
if (voteID === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await reloadVoteDetail(voteID);
|
||||||
|
});
|
||||||
|
|
||||||
|
const mySinglePick = ref(0);
|
||||||
|
const myMultiPick = ref<number[]>([]);
|
||||||
|
|
||||||
|
async function submitVote() {
|
||||||
|
if (currentVote.value === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const selection = currentVote.value.voteInfo.multipleOptions !== 1 ? myMultiPick.value : [mySinglePick.value];
|
||||||
|
|
||||||
|
if (selection.length === 0) {
|
||||||
|
toasts.danger({
|
||||||
|
title: "오류",
|
||||||
|
body: "선택한 항목이 없습니다.",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const voteID = currentVote.value.voteInfo.id;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await SammoAPI.Vote.Vote({
|
||||||
|
voteID,
|
||||||
|
selection,
|
||||||
|
});
|
||||||
|
|
||||||
|
toasts.success({
|
||||||
|
title: "성공",
|
||||||
|
body: "설문을 마쳤습니다.",
|
||||||
|
});
|
||||||
|
if (result.wonLottery) {
|
||||||
|
toasts.info({
|
||||||
|
title: "성공",
|
||||||
|
body: "특별한 설문 보상이 제공되었습니다!",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
void reloadVoteDetail(voteID);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
if (isString(e)) {
|
||||||
|
toasts.danger({
|
||||||
|
title: "에러",
|
||||||
|
body: e,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const myComment = ref("");
|
||||||
|
async function submitComment() {
|
||||||
|
if (currentVote.value === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const voteID = currentVote.value.voteInfo.id;
|
||||||
|
const text = myComment.value;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await SammoAPI.Vote.AddComment({
|
||||||
|
voteID,
|
||||||
|
text,
|
||||||
|
});
|
||||||
|
|
||||||
|
toasts.success({
|
||||||
|
title: "성공",
|
||||||
|
body: "댓글을 달았습니다.",
|
||||||
|
});
|
||||||
|
myComment.value = "";
|
||||||
|
void reloadVoteDetail(voteID);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
if (isString(e)) {
|
||||||
|
toasts.danger({
|
||||||
|
title: "에러",
|
||||||
|
body: e,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newVoteInfo = reactive<Omit<VoteInfo, "id" | "startDate">>({
|
||||||
|
title: "",
|
||||||
|
multipleOptions: 1,
|
||||||
|
endDate: undefined,
|
||||||
|
options: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const newVoteOptionsLength = ref(0);
|
||||||
|
const newVoteOptionsText = ref("");
|
||||||
|
|
||||||
|
watch(newVoteOptionsText, (newValue) => {
|
||||||
|
const lines = newValue.split("\n");
|
||||||
|
newVoteInfo.options = lines.filter((v) => v.length > 0);
|
||||||
|
newVoteOptionsLength.value = lines.length;
|
||||||
|
});
|
||||||
|
|
||||||
|
async function reloadVoteDetail(voteID: number) {
|
||||||
|
try {
|
||||||
|
const result = await SammoAPI.Vote.GetVoteDetail({ voteID });
|
||||||
|
currentVote.value = result;
|
||||||
|
|
||||||
|
if (voteID !== currentVoteID.value) {
|
||||||
|
myMultiPick.value = [];
|
||||||
|
mySinglePick.value = 0;
|
||||||
|
myComment.value = "";
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
if (isString(e)) {
|
||||||
|
toasts.danger({
|
||||||
|
title: "에러",
|
||||||
|
body: e,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reloadVote() {
|
||||||
|
try {
|
||||||
|
const result = await SammoAPI.Vote.GetVoteList();
|
||||||
|
const newVoteList = new Map<number, VoteInfo>();
|
||||||
|
for (const [rawVoteID, info] of Object.entries(result.votes).reverse()) {
|
||||||
|
newVoteList.set(parseInt(rawVoteID), info);
|
||||||
|
}
|
||||||
|
voteList.value = newVoteList;
|
||||||
|
if (newVoteList.size > 0 && currentVoteID.value === undefined) {
|
||||||
|
currentVoteID.value = newVoteList.keys().next().value;
|
||||||
|
} else if (currentVoteID.value !== undefined) {
|
||||||
|
void reloadVoteDetail(currentVoteID.value);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
if (isString(e)) {
|
||||||
|
toasts.danger({
|
||||||
|
title: "에러",
|
||||||
|
body: e,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(function () {
|
||||||
|
void reloadVote();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function submitNewVote() {
|
||||||
|
try {
|
||||||
|
await SammoAPI.Vote.NewVote(newVoteInfo);
|
||||||
|
toasts.success({
|
||||||
|
title: "성공",
|
||||||
|
body: "설문 조사가 생성되었습니다.",
|
||||||
|
});
|
||||||
|
void reloadVote();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
if (isString(e)) {
|
||||||
|
toasts.danger({
|
||||||
|
title: "에러",
|
||||||
|
body: e,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "@scss/common/break_500px.scss";
|
||||||
|
|
||||||
|
#vote-title {
|
||||||
|
font-size: 2em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#vote-old-title {
|
||||||
|
font-size: 1.5em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#vote-result {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
padding-left: 1ch;
|
||||||
|
padding-right: 1ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-idx {
|
||||||
|
width: 5ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-count {
|
||||||
|
width: 55px;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-percent {
|
||||||
|
width: 70px;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#vote-comment {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
padding-left: 1ch;
|
||||||
|
padding-right: 1ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-idx {
|
||||||
|
width: 5ch;
|
||||||
|
text-align: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-name {
|
||||||
|
width: 110px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr {
|
||||||
|
border-top: solid gray 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include media-1000px {
|
||||||
|
#vote-comment {
|
||||||
|
.comment-name {
|
||||||
|
width: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-date {
|
||||||
|
width: 98px;
|
||||||
|
padding-left: 0.5ch;
|
||||||
|
padding-right: 0.5ch;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include media-500px {
|
||||||
|
#vote-comment {
|
||||||
|
.comment-name {
|
||||||
|
width: 130px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-date {
|
||||||
|
width: 50px;
|
||||||
|
padding-left: 0.5ch;
|
||||||
|
padding-right: 0.5ch;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -34,6 +34,7 @@
|
|||||||
"v_processing": "v_processing.ts",
|
"v_processing": "v_processing.ts",
|
||||||
"v_nationBetting": "v_nationBetting.ts",
|
"v_nationBetting": "v_nationBetting.ts",
|
||||||
"v_nationGeneral": "v_nationGeneral.ts",
|
"v_nationGeneral": "v_nationGeneral.ts",
|
||||||
"v_globalDiplomacy": "v_globalDiplomacy"
|
"v_globalDiplomacy": "v_globalDiplomacy",
|
||||||
|
"v_vote": "v_vote.ts"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,7 +5,7 @@ export type VoteInfo = {
|
|||||||
title: string;
|
title: string;
|
||||||
multipleOptions: number;
|
multipleOptions: number;
|
||||||
startDate: string;
|
startDate: string;
|
||||||
endDate: string;
|
endDate?: string;
|
||||||
options: string[];
|
options: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,6 +16,7 @@ export type VoteComment = {
|
|||||||
nationName: string;
|
nationName: string;
|
||||||
generalName: string;
|
generalName: string;
|
||||||
text: string;
|
text: string;
|
||||||
|
date: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type VoteListResult = ValidResponse & {
|
export type VoteListResult = ValidResponse & {
|
||||||
@@ -26,4 +27,6 @@ export type VoteDetailResult = ValidResponse & {
|
|||||||
voteInfo: VoteInfo,
|
voteInfo: VoteInfo,
|
||||||
votes: [number[], number][],
|
votes: [number[], number][],
|
||||||
comments: VoteComment[],
|
comments: VoteComment[],
|
||||||
|
myVote: null|number[],
|
||||||
|
userCnt: number,
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import _colorNames from 'css-color-names';
|
||||||
|
|
||||||
|
const colorNames = _colorNames as Record<string, string>;
|
||||||
|
|
||||||
|
const colors: string[] = [
|
||||||
|
"red", "orange", "yellow", "green", "blue", "navy", "purple"
|
||||||
|
].map(color => colorNames[color]);
|
||||||
|
|
||||||
|
export function formatVoteColor(type: number): string {
|
||||||
|
return colors[type % colors.length];
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import "@scss/vote.scss";
|
||||||
|
|
||||||
|
import { createApp } from 'vue'
|
||||||
|
import PageVote from '@/PageVote.vue';
|
||||||
|
import { BootstrapVue3, BToastPlugin } from 'bootstrap-vue-3'
|
||||||
|
import { auto500px } from "./util/auto500px";
|
||||||
|
import { htmlReady } from "./util/htmlReady";
|
||||||
|
import { insertCustomCSS } from "./util/customCSS";
|
||||||
|
|
||||||
|
auto500px();
|
||||||
|
|
||||||
|
htmlReady(() => {
|
||||||
|
insertCustomCSS();
|
||||||
|
});
|
||||||
|
createApp(PageVote).use(BootstrapVue3).use(BToastPlugin).mount('#app')
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace sammo;
|
||||||
|
|
||||||
|
include "lib.php";
|
||||||
|
include "func.php";
|
||||||
|
//로그인 검사
|
||||||
|
$session = Session::requireLogin()->loginGame()->setReadOnly();
|
||||||
|
$userID = Session::getUserID();
|
||||||
|
$isVoteAdmin = in_array('vote', $session->acl[DB::prefix()] ?? []);
|
||||||
|
$isVoteAdmin = $isVoteAdmin || $session->userGrade >= 5;
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=500" />
|
||||||
|
<title><?= UniqueConst::$serverName ?>: 설문 조사</title>
|
||||||
|
<?= WebUtil::printStaticValues(['staticValues' => [
|
||||||
|
'serverNick' => DB::prefix(),
|
||||||
|
'serverID' => UniqueConst::$serverID,
|
||||||
|
'isGameLoggedIn' => $session->isGameLoggedIn(),
|
||||||
|
'isVoteAdmin' => $isVoteAdmin,
|
||||||
|
]]) ?>
|
||||||
|
<?= WebUtil::printJS('../d_shared/common_path.js', true) ?>
|
||||||
|
<?= WebUtil::printDist('vue', ['v_vote'], true) ?>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id='app'>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user