feat: complete map and trend frontend flows
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useMediaQuery, useMouseInElement } from '@vueuse/core';
|
||||
import { useElementSize, useMediaQuery, useMouseInElement } from '@vueuse/core';
|
||||
import SkeletonLines from '../ui/SkeletonLines.vue';
|
||||
import MapCityBasic from './MapCityBasic.vue';
|
||||
import MapCityDetail from './MapCityDetail.vue';
|
||||
@@ -72,6 +72,8 @@ const mapStore = useMapViewerStore();
|
||||
const { showCityName, detailMode, hoveredCityId, selectedCityId } = storeToRefs(mapStore);
|
||||
|
||||
const mapArea = ref<HTMLElement | null>(null);
|
||||
const mapBody = ref<HTMLElement | null>(null);
|
||||
const { width: mapBodyWidth } = useElementSize(mapBody);
|
||||
const { elementX, elementY } = useMouseInElement(mapArea);
|
||||
|
||||
const resolveSeason = (month: number): string => {
|
||||
@@ -131,7 +133,15 @@ const dynamicCityById = computed(() => {
|
||||
return map;
|
||||
});
|
||||
|
||||
const mapScale = computed(() => (isWide.value ? 1 : SMALL_MAP_SCALE));
|
||||
const mapScale = computed(() => {
|
||||
if (isWide.value) {
|
||||
return 1;
|
||||
}
|
||||
if (mapBodyWidth.value <= 0) {
|
||||
return SMALL_MAP_SCALE;
|
||||
}
|
||||
return Math.min(SMALL_MAP_SCALE, mapBodyWidth.value / BASE_MAP_WIDTH);
|
||||
});
|
||||
|
||||
const mapWidth = computed(() => `${BASE_MAP_WIDTH * mapScale.value}px`);
|
||||
|
||||
@@ -285,7 +295,7 @@ const selectCity = (cityId: number) => {
|
||||
<div v-else-if="!props.mapData || !props.mapLayout" class="map-empty">
|
||||
지도 데이터를 불러오지 못했습니다.
|
||||
</div>
|
||||
<div v-else class="map-body">
|
||||
<div v-else ref="mapBody" class="map-body">
|
||||
<div
|
||||
ref="mapArea"
|
||||
class="map-area"
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { formatLog } from '../../utils/formatLog';
|
||||
|
||||
type LogEntry = {
|
||||
id: number;
|
||||
text: string;
|
||||
};
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
logs?: LogEntry[] | null;
|
||||
emptyText?: string;
|
||||
}>(),
|
||||
{
|
||||
logs: null,
|
||||
emptyText: '기록 없음',
|
||||
}
|
||||
);
|
||||
|
||||
const formattedLogs = computed(() =>
|
||||
(props.logs ?? []).map((entry) => ({
|
||||
id: entry.id,
|
||||
html: formatLog(entry.text),
|
||||
}))
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="recent-log-list">
|
||||
<template v-if="formattedLogs.length">
|
||||
<!-- 레거시 색상 tag만 formatLog가 span으로 변환한다. -->
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<div v-for="entry in formattedLogs" :key="entry.id" class="recent-log-line" v-html="entry.html" />
|
||||
</template>
|
||||
<div v-else class="recent-log-empty">{{ emptyText }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.recent-log-list {
|
||||
min-width: 0;
|
||||
color: #fff;
|
||||
font-family: 'Times New Roman', serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.recent-log-line {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.recent-log-empty {
|
||||
color: #aaa;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -24,11 +24,13 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
||||
type CommandTable = Awaited<ReturnType<typeof trpc.turns.getCommandTable.query>>;
|
||||
type MessageBundle = Awaited<ReturnType<typeof trpc.messages.getRecent.query>>;
|
||||
type MessageContacts = Awaited<ReturnType<typeof trpc.messages.getContacts.query>>;
|
||||
type FrontRecords = Awaited<ReturnType<typeof trpc.general.getFrontRecords.query>>;
|
||||
type BoardAccess = Awaited<ReturnType<typeof trpc.board.getAccess.query>>;
|
||||
type ReservedTurnView = Awaited<ReturnType<typeof trpc.turns.reserved.getGeneral.query>>[number];
|
||||
|
||||
const loading = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
const frontRecordsError = ref<string | null>(null);
|
||||
const realtimeEnabled = ref(true);
|
||||
const realtimeStatus = ref<'idle' | 'connected' | 'paused'>('idle');
|
||||
|
||||
@@ -39,6 +41,7 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
||||
const commandTable = ref<CommandTable | null>(null);
|
||||
const messages = ref<MessageBundle | null>(null);
|
||||
const messageContacts = ref<MessageContacts | null>(null);
|
||||
const frontRecords = ref<FrontRecords | null>(null);
|
||||
const boardAccess = ref<BoardAccess | null>(null);
|
||||
const reservedGeneralTurns = ref<ReservedTurnView[] | null>(null);
|
||||
const reservedNationTurns = ref<ReservedTurnView[] | null>(null);
|
||||
@@ -197,6 +200,7 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
||||
}
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
frontRecordsError.value = null;
|
||||
|
||||
try {
|
||||
const context = await trpc.general.me.query();
|
||||
@@ -217,7 +221,11 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
||||
context.general.nationId > 0 && context.general.officerLevel >= 5
|
||||
? trpc.turns.reserved.getNation.query({ generalId: id })
|
||||
: Promise.resolve(null);
|
||||
const [layout, lobby, map, commands, messageData, contacts, access, generalTurns, nationTurns] =
|
||||
const frontRecordsPromise = trpc.general.getFrontRecords.query().catch((err: unknown) => {
|
||||
frontRecordsError.value = resolveErrorMessage(err);
|
||||
return null;
|
||||
});
|
||||
const [layout, lobby, map, commands, messageData, contacts, access, records, generalTurns, nationTurns] =
|
||||
await Promise.all([
|
||||
layoutPromise,
|
||||
trpc.lobby.info.query(),
|
||||
@@ -226,6 +234,7 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
||||
trpc.messages.getRecent.query({ generalId: id }),
|
||||
trpc.messages.getContacts.query({ generalId: id }),
|
||||
trpc.board.getAccess.query(),
|
||||
frontRecordsPromise,
|
||||
generalTurnsPromise,
|
||||
nationTurnsPromise,
|
||||
]);
|
||||
@@ -237,6 +246,7 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
||||
messages.value = messageData;
|
||||
messageContacts.value = contacts;
|
||||
boardAccess.value = access;
|
||||
frontRecords.value = records;
|
||||
reservedGeneralTurns.value = generalTurns;
|
||||
reservedNationTurns.value = nationTurns;
|
||||
if (initializedMailboxGeneralId !== id) {
|
||||
@@ -587,6 +597,7 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
||||
return {
|
||||
loading,
|
||||
error,
|
||||
frontRecordsError,
|
||||
realtimeEnabled,
|
||||
realtimeStatus,
|
||||
generalContext,
|
||||
@@ -600,6 +611,7 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
||||
commandTable,
|
||||
messages,
|
||||
messageContacts,
|
||||
frontRecords,
|
||||
boardAccess,
|
||||
reservedGeneralTurns,
|
||||
reservedNationTurns,
|
||||
|
||||
@@ -11,6 +11,7 @@ import CityBasicCard from '../components/main/CityBasicCard.vue';
|
||||
import NationBasicCard from '../components/main/NationBasicCard.vue';
|
||||
import MessagePanel from '../components/main/MessagePanel.vue';
|
||||
import SelectedCityPanel from '../components/main/SelectedCityPanel.vue';
|
||||
import RecentLogList from '../components/main/RecentLogList.vue';
|
||||
import { useSessionStore } from '../stores/session';
|
||||
import { useMainDashboardStore } from '../stores/mainDashboard';
|
||||
import { trpc } from '../utils/trpc';
|
||||
@@ -35,16 +36,17 @@ const tournamentStage = ref(0);
|
||||
const {
|
||||
loading,
|
||||
error,
|
||||
frontRecordsError,
|
||||
realtimeEnabled,
|
||||
general,
|
||||
city,
|
||||
nation,
|
||||
lobbyInfo,
|
||||
worldMap,
|
||||
mapLayout,
|
||||
selectedCity,
|
||||
commandTable,
|
||||
messages,
|
||||
frontRecords,
|
||||
boardAccess,
|
||||
reservedGeneralTurns,
|
||||
reservedNationTurns,
|
||||
@@ -206,19 +208,18 @@ watch(
|
||||
<div v-if="mobileTab === 'world'" class="mobile-panel">
|
||||
<PanelCard title="장수 동향">
|
||||
<SkeletonLines v-if="loading" :lines="4" />
|
||||
<div v-else class="placeholder">장수 동향은 실시간 스트림으로 연결 예정</div>
|
||||
<div v-else-if="frontRecordsError" class="record-error" role="alert">{{ frontRecordsError }}</div>
|
||||
<RecentLogList v-else :logs="frontRecords?.global" />
|
||||
</PanelCard>
|
||||
<PanelCard title="개인 기록">
|
||||
<SkeletonLines v-if="loading" :lines="4" />
|
||||
<div v-else class="placeholder">개인 기록 영역</div>
|
||||
<div v-else-if="frontRecordsError" class="record-error" role="alert">{{ frontRecordsError }}</div>
|
||||
<RecentLogList v-else :logs="frontRecords?.general" />
|
||||
</PanelCard>
|
||||
<PanelCard title="중원 정세">
|
||||
<SkeletonLines v-if="loading" :lines="4" />
|
||||
<div v-else class="placeholder">
|
||||
<div>유저 {{ lobbyInfo?.userCnt ?? '-' }} / {{ lobbyInfo?.maxUserCnt ?? '-' }}</div>
|
||||
<div>NPC {{ lobbyInfo?.npcCnt ?? '-' }}</div>
|
||||
<div>세력 {{ lobbyInfo?.nationCnt ?? '-' }}</div>
|
||||
</div>
|
||||
<div v-else-if="frontRecordsError" class="record-error" role="alert">{{ frontRecordsError }}</div>
|
||||
<RecentLogList v-else :logs="frontRecords?.history" />
|
||||
</PanelCard>
|
||||
</div>
|
||||
|
||||
@@ -255,12 +256,9 @@ watch(
|
||||
<SelectedCityPanel :city="selectedCity" :loading="loading" />
|
||||
</PanelCard>
|
||||
<PanelCard title="중원 정세">
|
||||
<SkeletonLines v-if="loading" :lines="3" />
|
||||
<div v-else class="placeholder">
|
||||
<div>유저 {{ lobbyInfo?.userCnt ?? '-' }} / {{ lobbyInfo?.maxUserCnt ?? '-' }}</div>
|
||||
<div>NPC {{ lobbyInfo?.npcCnt ?? '-' }}</div>
|
||||
<div>세력 {{ lobbyInfo?.nationCnt ?? '-' }}</div>
|
||||
</div>
|
||||
<SkeletonLines v-if="loading" :lines="4" />
|
||||
<div v-else-if="frontRecordsError" class="record-error" role="alert">{{ frontRecordsError }}</div>
|
||||
<RecentLogList v-else :logs="frontRecords?.history" />
|
||||
</PanelCard>
|
||||
</div>
|
||||
|
||||
@@ -284,7 +282,8 @@ watch(
|
||||
</PanelCard>
|
||||
<PanelCard title="장수 동향">
|
||||
<SkeletonLines v-if="loading" :lines="4" />
|
||||
<div v-else class="placeholder">장수 동향은 실시간 스트림으로 연결 예정</div>
|
||||
<div v-else-if="frontRecordsError" class="record-error" role="alert">{{ frontRecordsError }}</div>
|
||||
<RecentLogList v-else :logs="frontRecords?.global" />
|
||||
</PanelCard>
|
||||
<PanelCard title="도시 정보">
|
||||
<CityBasicCard :city="city" :loading="loading" />
|
||||
@@ -294,7 +293,8 @@ watch(
|
||||
</PanelCard>
|
||||
<PanelCard title="개인 기록">
|
||||
<SkeletonLines v-if="loading" :lines="4" />
|
||||
<div v-else class="placeholder">개인 기록 영역</div>
|
||||
<div v-else-if="frontRecordsError" class="record-error" role="alert">{{ frontRecordsError }}</div>
|
||||
<RecentLogList v-else :logs="frontRecords?.general" />
|
||||
</PanelCard>
|
||||
</div>
|
||||
<MessagePanel
|
||||
@@ -339,6 +339,10 @@ watch(
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.record-error {
|
||||
color: #ff8a80;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 1.6rem;
|
||||
font-weight: 600;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useMediaQuery } from '@vueuse/core';
|
||||
import PanelCard from '../components/ui/PanelCard.vue';
|
||||
import SkeletonLines from '../components/ui/SkeletonLines.vue';
|
||||
import MapViewer from '../components/main/MapViewer.vue';
|
||||
import RecentLogList from '../components/main/RecentLogList.vue';
|
||||
import { trpc } from '../utils/trpc';
|
||||
import { useSessionStore } from '../stores/session';
|
||||
|
||||
@@ -121,12 +122,7 @@ onMounted(() => {
|
||||
</PanelCard>
|
||||
<PanelCard title="중원 정세">
|
||||
<SkeletonLines v-if="loading" :lines="3" />
|
||||
<div v-else class="placeholder">
|
||||
<div>유저 {{ worldTrend?.userCnt ?? '-' }} / {{ worldTrend?.maxUserCnt ?? '-' }}</div>
|
||||
<div>NPC {{ worldTrend?.npcCnt ?? '-' }}</div>
|
||||
<div>세력 {{ worldTrend?.nationCnt ?? '-' }}</div>
|
||||
<div>상성 {{ worldTrend?.fictionMode ?? '-' }}</div>
|
||||
</div>
|
||||
<RecentLogList v-else :logs="mapData?.history" />
|
||||
</PanelCard>
|
||||
<PanelCard title="세력 일람">
|
||||
<SkeletonLines v-if="loading" :lines="4" />
|
||||
@@ -193,13 +189,7 @@ onMounted(() => {
|
||||
</PanelCard>
|
||||
<PanelCard title="중원 정세">
|
||||
<SkeletonLines v-if="loading" :lines="3" />
|
||||
<div v-else class="placeholder">
|
||||
<div>유저 {{ worldTrend?.userCnt ?? '-' }} / {{ worldTrend?.maxUserCnt ?? '-' }}</div>
|
||||
<div>NPC {{ worldTrend?.npcCnt ?? '-' }}</div>
|
||||
<div>세력 {{ worldTrend?.nationCnt ?? '-' }}</div>
|
||||
<div>상성 {{ worldTrend?.fictionMode ?? '-' }}</div>
|
||||
<div>기타 {{ worldTrend?.otherTextInfo ?? '-' }}</div>
|
||||
</div>
|
||||
<RecentLogList v-else :logs="mapData?.history" />
|
||||
</PanelCard>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ type MapLayout = Awaited<ReturnType<typeof trpc.public.getMapLayout.query>>;
|
||||
type HistoryData = {
|
||||
year: number;
|
||||
month: number;
|
||||
map: Awaited<ReturnType<typeof trpc.public.getCachedMap.query>>;
|
||||
map: Omit<Awaited<ReturnType<typeof trpc.public.getCachedMap.query>>, 'history'>;
|
||||
nations: Array<{
|
||||
id: number;
|
||||
name: string;
|
||||
|
||||
Reference in New Issue
Block a user