정리
This commit is contained in:
@@ -61,42 +61,35 @@
|
||||
<Dialog v-model:visible="memberDialog" :style="{width: '450px'}"
|
||||
:header="dialogMode === 'new' ? '새 회원 추가' : '회원 정보 수정'"
|
||||
:modal="true" class="p-fluid">
|
||||
<div class="field">
|
||||
<label for="name">이름</label>
|
||||
<InputText id="name" v-model="member.name" required autofocus />
|
||||
<!-- 회원 정보 다이얼로그 내부 폼 예시 -->
|
||||
<div class="field mb-3">
|
||||
<label for="gender" class="mb-1">성별</label>
|
||||
<Select id="gender" v-model="member.gender" :options="genderOptions" optionLabel="name" placeholder="성별 선택" class="w-full" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="gender">성별</label>
|
||||
<Dropdown id="gender" v-model="member.gender" :options="genderOptions"
|
||||
optionLabel="name" placeholder="성별 선택" />
|
||||
<div class="field mb-3">
|
||||
<label for="memberType" class="mb-1">회원 유형</label>
|
||||
<Select id="memberType" v-model="member.memberTypeObj" :options="memberTypes" optionLabel="name" placeholder="회원 유형 선택" class="w-full" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="memberType">회원 유형</label>
|
||||
<Dropdown id="memberType" v-model="member.memberTypeObj" :options="memberTypes"
|
||||
optionLabel="name" placeholder="회원 유형 선택" />
|
||||
<div class="field mb-3">
|
||||
<label for="phone" class="mb-1">전화번호</label>
|
||||
<InputText id="phone" v-model="member.phone" class="w-full" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="phone">전화번호</label>
|
||||
<InputText id="phone" v-model="member.phone" />
|
||||
<div class="field mb-3">
|
||||
<label for="email" class="mb-1">이메일</label>
|
||||
<InputText id="email" v-model="member.email" class="w-full" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="email">이메일</label>
|
||||
<InputText id="email" v-model="member.email" />
|
||||
<div class="field mb-3">
|
||||
<label for="handicap" class="mb-1">핸디캡</label>
|
||||
<InputText id="handicap" v-model="member.handicap" type="number" min="0" max="50" class="w-full" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="handicap">핸디캡</label>
|
||||
<InputText id="handicap" v-model="member.handicap" type="number" min="0" max="50" />
|
||||
<div class="field mb-3">
|
||||
<label for="status" class="mb-1">상태</label>
|
||||
<Select id="status" v-model="member.status" :options="statusOptions" optionLabel="label" optionValue="value" placeholder="상태 선택" class="w-full" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="status">상태</label>
|
||||
<Dropdown id="status" v-model="member.status" :options="statusOptions"
|
||||
optionLabel="label" optionValue="value" placeholder="상태 선택" />
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-content-end gap-2 mt-4">
|
||||
<Button label="취소" icon="pi pi-times" class="p-button-text" @click="hideDialog" />
|
||||
<Button label="저장" icon="pi pi-check" class="p-button-text" @click="saveMember" />
|
||||
</template>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
<!-- 삭제 확인 다이얼로그 -->
|
||||
@@ -177,13 +170,6 @@
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import Button from 'primevue/button';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import MultiSelect from 'primevue/multiselect';
|
||||
import InputNumber from 'primevue/inputnumber';
|
||||
import Dialog from 'primevue/dialog';
|
||||
import DataTable from 'primevue/datatable';
|
||||
import Column from 'primevue/column';
|
||||
import apiClient from '@/services/api';
|
||||
import clubService from '@/services/clubService';
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-12 md:col-4">
|
||||
<Dropdown v-model="filters.team" :options="teamOptions" optionLabel="name"
|
||||
<Select v-model="filters.team" :options="teamOptions" optionLabel="name"
|
||||
placeholder="팀 선택" class="w-full" />
|
||||
</div>
|
||||
<div class="col-12 md:col-4">
|
||||
@@ -118,11 +118,11 @@
|
||||
<div class="ranking-filters mb-3">
|
||||
<div class="grid">
|
||||
<div class="col-12 md:col-4">
|
||||
<Dropdown v-model="rankingType" :options="rankingOptions" optionLabel="name"
|
||||
<Select v-model="rankingType" :options="rankingOptions" optionLabel="name"
|
||||
placeholder="순위 유형" class="w-full" />
|
||||
</div>
|
||||
<div class="col-12 md:col-4">
|
||||
<Dropdown v-model="rankingGame" :options="gameRankingOptions" optionLabel="name"
|
||||
<Select v-model="rankingGame" :options="gameRankingOptions" optionLabel="name"
|
||||
placeholder="게임 선택" class="w-full" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -177,7 +177,7 @@
|
||||
<div class="export-options">
|
||||
<div class="field">
|
||||
<label for="exportFormat">파일 형식</label>
|
||||
<Dropdown id="exportFormat" v-model="exportFormat" :options="exportFormatOptions"
|
||||
<Select id="exportFormat" v-model="exportFormat" :options="exportFormatOptions"
|
||||
optionLabel="name" placeholder="형식 선택" class="w-full" />
|
||||
</div>
|
||||
<div class="field">
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
width: 100%;
|
||||
max-width: 700px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
background-color: var(--p-gray-50);
|
||||
}
|
||||
|
||||
.event-public-wrapper>.p-card>.p-card-body {
|
||||
@@ -251,7 +253,11 @@
|
||||
.team-score-table-wrapper {
|
||||
overflow-x: auto;
|
||||
border-radius: 10px;
|
||||
padding: 12px 8px;
|
||||
padding: 0
|
||||
}
|
||||
|
||||
.team-score-table-wrapper .p-card-body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.event-public-card {
|
||||
@@ -401,7 +407,7 @@
|
||||
}
|
||||
|
||||
.team-name-strong {
|
||||
font-size: 2.3rem;
|
||||
font-size: 2rem;
|
||||
font-weight: 900;
|
||||
line-height: 1.1;
|
||||
margin-bottom: 2px;
|
||||
|
||||
@@ -1,27 +1,31 @@
|
||||
<template>
|
||||
<div class="event-public-wrapper">
|
||||
<h1 v-if="clubName" class="p-text-primary p-text-bold p-text-lg p-mb-2">{{ clubName }}</h1>
|
||||
<Divider class="p-mb-4" />
|
||||
<div class="p-shadow-8 p-p-5 p-rounded-xl event-public-card">
|
||||
<div>
|
||||
<div v-if="loading" class="p-text-center p-mb-4">로딩 중...</div>
|
||||
<div v-else-if="error" class="p-text-center p-mb-4">{{ error }}</div>
|
||||
<div v-else-if="!isAuthenticated">
|
||||
<div class="event-password-form flex flex-column p-ai-center p-mt-6 p-mb-6">
|
||||
<i class="pi pi-lock p-mb-3 event-password-lock"></i>
|
||||
<div class="p-text-center p-mb-3 p-text-lg event-password-desc">이 이벤트는 비밀번호가 필요합니다.<br>비밀번호를 입력해 주세요.</div>
|
||||
<InputText v-model="inputPassword" type="password" placeholder="비밀번호 입력" class="p-mb-2 event-password-input"
|
||||
@keyup.enter="verifyPassword" autofocus />
|
||||
<Button label="확인" @click="verifyPassword" class="p-mb-2 event-password-btn" :loading="loading" />
|
||||
<div v-if="passwordError" class="p-error p-mt-1 event-password-error">{{ passwordError }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="event-public-card">
|
||||
<div v-if="loading" class="p-text-center p-mb-4">로딩 중...</div>
|
||||
<div v-else-if="error" class="p-text-center p-mb-4">{{ error }}</div>
|
||||
<div v-else-if="!isAuthenticated">
|
||||
<Card>
|
||||
<template #content>
|
||||
<div class="event-password-form flex flex-column p-ai-center p-mt-6 p-mb-6">
|
||||
<i class="pi pi-lock p-mb-3 event-password-lock"></i>
|
||||
<div class="p-text-center p-mb-3 p-text-lg event-password-desc">이 이벤트는 비밀번호가 필요합니다.<br>비밀번호를 입력해 주세요.</div>
|
||||
<InputText v-model="inputPassword" type="password" placeholder="비밀번호 입력" class="p-mb-2 event-password-input"
|
||||
@keyup.enter="verifyPassword" autofocus />
|
||||
<Button label="확인" @click="verifyPassword" class="p-mb-2 event-password-btn" :loading="loading" />
|
||||
<div v-if="passwordError" class="p-error p-mt-1 event-password-error">{{ passwordError }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</div>
|
||||
<div v-else>
|
||||
<template v-if="event">
|
||||
<Divider class="p-mb-4" />
|
||||
<div class="event-tag">
|
||||
<Tag :value="event.eventType" class="event-type-tag" />
|
||||
<Tag :value="event.status"
|
||||
:severity="event.status === '완료' ? 'success' : event.status === '활성' ? 'info' : 'warning'"
|
||||
class="p-ml-4" />
|
||||
class="ml-1" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex justify-content-end gap-2">
|
||||
@@ -57,8 +61,10 @@
|
||||
<div class="p-text-secondary p-mb-4">{{ event.description || '' }}</div>
|
||||
<Divider />
|
||||
<!-- 팀별 게임별 점수/등수 표 -->
|
||||
<template v-if="teams.length > 0">
|
||||
<div class="team-score-table-wrapper p-mt-4">
|
||||
</div>
|
||||
<template v-if="teams.length > 0">
|
||||
<Card class="team-score-table-wrapper mb-4">
|
||||
<template #content>
|
||||
<DataTable :value="getAllTeamsWithUnassigned()" class="team-score-table" :responsiveLayout="'scroll'">
|
||||
<Column header="팀">
|
||||
<template #body="slotProps">
|
||||
@@ -88,13 +94,65 @@
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
</div>
|
||||
<div v-for="team in getAllTeamsWithUnassigned()" :key="team.id">
|
||||
</template>
|
||||
</Card>
|
||||
<div v-for="team in getAllTeamsWithUnassigned()" :key="team.id" class="mb-4">
|
||||
<Card>
|
||||
<template #content>
|
||||
<MemberListCard
|
||||
:title="parseInt(team.teamNumber) ? '팀 ' + team.teamNumber : '미배정 인원'"
|
||||
:members="team.members"
|
||||
:teamHandicap="team.handicap"
|
||||
:teamNumber="team.teamNumber"
|
||||
:gameCount="event.gameCount"
|
||||
:canInputScore="canInputScore"
|
||||
:scoresProxy="scoresProxy"
|
||||
:scoreStates="scoreStates"
|
||||
:onScoreInput="onScoreInput"
|
||||
:onScoreBlur="onScoreBlur"
|
||||
:onScoreEnter="onScoreEnter"
|
||||
:getMemberTotalScore="getMemberTotalScore"
|
||||
:getMemberAverageScore="getMemberAverageScore"
|
||||
:scoreColorClass="scoreColorClass"
|
||||
/>
|
||||
<!-- 팀 카드 요약(총점/등수/게임별 점수)은 '팀'만 노출 -->
|
||||
<Divider />
|
||||
<div v-if="team.teamNumber" class="team-game-scores">
|
||||
<div class="team-total-row team-total-row-responsive team-total-row-block">
|
||||
<div class="team-games-grid"
|
||||
:style="{ gridTemplateColumns: `repeat(${event.gameCount > 3 ? 3 : event.gameCount}, 1fr)` }">
|
||||
<template v-for="n in event.gameCount" :key="n">
|
||||
<div class="team-game-cell">
|
||||
<b>{{ n }}G:</b> {{ getTeamTotalScore(team, n) }}
|
||||
<span v-if="getRanksByGame(n)[(team.id || team.teamNumber) + ''] !== undefined"
|
||||
class="rank-badge"
|
||||
:class="'rank-' + getRanksByGame(n)[(team.id || team.teamNumber) + '']">
|
||||
({{ getRanksByGame(n)[(team.id || team.teamNumber) + ''] }}위)
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="team-total-main">
|
||||
<span class="team-total-label">총점</span>
|
||||
<span class="team-total-scores">{{ Array.from({ length: event.gameCount }, (_, i) => getTeamTotalScore(team, i + 1)).reduce((a, b) => a + b, 0) }}</span>
|
||||
<span v-if="getTeamOverallRank({ ...team, id: (team.id || team.teamNumber) + '' })"
|
||||
class="rank-badge"
|
||||
:class="'rank-' + getTeamOverallRank({ ...team, id: (team.id || team.teamNumber) + '' })">
|
||||
{{ getTeamOverallRank({ ...team, id: (team.id || team.teamNumber) + '' }) }}위
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<Card>
|
||||
<template #content>
|
||||
<MemberListCard
|
||||
:title="parseInt(team.teamNumber) ? '팀 ' + team.teamNumber : '미배정 인원'"
|
||||
:members="team.members"
|
||||
:teamHandicap="team.handicap"
|
||||
:teamNumber="team.teamNumber"
|
||||
title="참가자"
|
||||
:members="sortedParticipants"
|
||||
:gameCount="event.gameCount"
|
||||
:canInputScore="canInputScore"
|
||||
:scoresProxy="scoresProxy"
|
||||
@@ -106,53 +164,10 @@
|
||||
:getMemberAverageScore="getMemberAverageScore"
|
||||
:scoreColorClass="scoreColorClass"
|
||||
/>
|
||||
<!-- 팀 카드 요약(총점/등수/게임별 점수)은 '팀'만 노출 -->
|
||||
<div v-if="team.teamNumber" class="team-game-scores">
|
||||
<div class="team-total-row team-total-row-responsive team-total-row-block">
|
||||
<div class="team-games-grid"
|
||||
:style="{ gridTemplateColumns: `repeat(${event.gameCount > 3 ? 3 : event.gameCount}, 1fr)` }">
|
||||
<template v-for="n in event.gameCount" :key="n">
|
||||
<div class="team-game-cell">
|
||||
<b>{{ n }}G:</b> {{ getTeamTotalScore(team, n) }}
|
||||
<span v-if="getRanksByGame(n)[(team.id || team.teamNumber) + ''] !== undefined"
|
||||
class="rank-badge"
|
||||
:class="'rank-' + getRanksByGame(n)[(team.id || team.teamNumber) + '']">
|
||||
({{ getRanksByGame(n)[(team.id || team.teamNumber) + ''] }}위)
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="team-total-main">
|
||||
<span class="team-total-label">총점</span>
|
||||
<span class="team-total-scores">{{ Array.from({ length: event.gameCount }, (_, i) => getTeamTotalScore(team, i + 1)).reduce((a, b) => a + b, 0) }}</span>
|
||||
<span v-if="getTeamOverallRank({ ...team, id: (team.id || team.teamNumber) + '' })"
|
||||
class="rank-badge"
|
||||
:class="'rank-' + getTeamOverallRank({ ...team, id: (team.id || team.teamNumber) + '' })">
|
||||
{{ getTeamOverallRank({ ...team, id: (team.id || team.teamNumber) + '' }) }}위
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<MemberListCard
|
||||
title="참가자"
|
||||
:members="sortedParticipants"
|
||||
:gameCount="event.gameCount"
|
||||
:canInputScore="canInputScore"
|
||||
:scoresProxy="scoresProxy"
|
||||
:scoreStates="scoreStates"
|
||||
:onScoreInput="onScoreInput"
|
||||
:onScoreBlur="onScoreBlur"
|
||||
:onScoreEnter="onScoreEnter"
|
||||
:getMemberTotalScore="getMemberTotalScore"
|
||||
:getMemberAverageScore="getMemberAverageScore"
|
||||
:scoreColorClass="scoreColorClass"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<JoinDialog v-if="showJoinDialog && canJoin" v-model:visible="showJoinDialog" :publicHash="publicHash"
|
||||
@@ -170,6 +185,7 @@ import MemberListCard from '@/components/public/MemberListCard.vue';
|
||||
import JoinDialog from '@/components/public/JoinDialog.vue';
|
||||
import dayjs from 'dayjs';
|
||||
import PublicEventService from '@/services/PublicEventService';
|
||||
import Panel from 'primevue/panel';
|
||||
|
||||
const route = useRoute();
|
||||
const publicHash = route.params.publicHash;
|
||||
@@ -346,18 +362,22 @@ async function fetchEventData(password) {
|
||||
try {
|
||||
const { status, data } = await PublicEventService.fetchEvent(publicHash, password);
|
||||
if (status === 403) {
|
||||
console.log('[fetchEventData] 403', { publicHash, password });
|
||||
error.value = '이벤트에 접근할 수 없습니다.';
|
||||
PublicEventService.removeToken(publicHash);
|
||||
event.value = null;
|
||||
return;
|
||||
}
|
||||
if (status === 401) {
|
||||
console.log('[fetchEventData] 401', { publicHash, password });
|
||||
isAuthenticated.value = false;
|
||||
PublicEventService.removeToken(publicHash);
|
||||
event.value = null;
|
||||
return;
|
||||
}
|
||||
if (status === 404 || (data && data.message === '이벤트를 찾을 수 없습니다.')) {
|
||||
error.value = data && data.message ? data.message : '존재하지 않는 이벤트입니다.';
|
||||
event.value = null;
|
||||
return;
|
||||
}
|
||||
console.log('[fetchEventData] 200', data);
|
||||
clubName.value = data.clubName || '';
|
||||
event.value = data.event;
|
||||
teams.value = data.teams || [];
|
||||
@@ -393,6 +413,7 @@ async function fetchEventData(password) {
|
||||
});
|
||||
} catch (e) {
|
||||
error.value = e.message;
|
||||
event.value = null;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user