merge: 최신 main을 Profile 포괄 권한 제거에 통합

This commit is contained in:
2026-08-17 16:11:07 +00:00
58 changed files with 4438 additions and 963 deletions
@@ -276,8 +276,7 @@ const stateClass = (state: number): string => {
<template>
<div class="map-preview" :class="`map-preview-${props.mode}`">
<div class="map-preview-header">
<span class="map-preview-title">{{ props.mapLayout.mapName }}</span>
<div class="map-preview-date-bar">
<span class="map-preview-date">{{ props.mapData.year }} {{ props.mapData.month }}</span>
</div>
<div ref="mapBody" class="map-preview-body" :style="{ backgroundImage: `url('${mapBackground}')` }">
@@ -358,18 +357,22 @@ const stateClass = (state: number): string => {
width: min(100%, 700px);
margin-inline: auto;
flex-direction: column;
gap: 6px;
}
.map-preview-header {
.map-preview-date-bar {
display: flex;
justify-content: space-between;
font-size: 0.7rem;
color: rgba(232, 221, 196, 0.7);
height: 20px;
justify-content: center;
background: #000;
color: rgba(232, 221, 196, 0.82);
}
.map-preview-title {
font-weight: 600;
.map-preview-date {
display: block;
width: 160px;
font-size: 14px;
line-height: 20px;
text-align: center;
}
.map-preview-body {
+31 -11
View File
@@ -700,12 +700,6 @@ const handleEnter = async (profile: LobbyProfile, targetPath: string) => {
class="map-preview-panel"
data-testid="public-map-preview-panel"
>
<div class="flex items-center justify-between text-xs text-zinc-400 mb-2">
<span class="font-semibold" :style="{ color: selectedMapProfile.color }">
{{ selectedMapProfile.korName }}섭
</span>
<span>{{ selectedMapProfile.status }}</span>
</div>
<div v-if="selectedMapPreview">
<MapPreview
:map-data="selectedMapPreview.mapData"
@@ -714,12 +708,18 @@ const handleEnter = async (profile: LobbyProfile, targetPath: string) => {
/>
<div
v-if="profileDetails[selectedMapProfile.profileName]"
class="text-xs text-zinc-400 mt-2"
class="map-preview-summary"
data-testid="public-map-preview-summary"
>
유저 {{ profileDetails[selectedMapProfile.profileName]?.userCnt ?? '-' }} /
{{ profileDetails[selectedMapProfile.profileName]?.maxUserCnt ?? '-' }} ·
{{ profileDetails[selectedMapProfile.profileName]?.nationCnt ?? '-' }}국 ·
{{ profileDetails[selectedMapProfile.profileName]?.turnTerm ?? '-' }}분 턴
<span class="map-preview-runtime-status" :style="{ color: selectedMapProfile.color }">
{{ selectedMapProfile.status }}
</span>
<span>
유저 {{ profileDetails[selectedMapProfile.profileName]?.userCnt ?? '-' }} /
{{ profileDetails[selectedMapProfile.profileName]?.maxUserCnt ?? '-' }}
</span>
<span>{{ profileDetails[selectedMapProfile.profileName]?.nationCnt ?? '-' }}국</span>
<span>{{ profileDetails[selectedMapProfile.profileName]?.turnTerm ?? '-' }}분 턴</span>
</div>
</div>
<div v-else class="text-xs text-zinc-500 py-8 text-center">지도를 불러오는 중...</div>
@@ -832,6 +832,26 @@ const handleEnter = async (profile: LobbyProfile, targetPath: string) => {
padding: 12px;
}
.map-preview-summary {
display: flex;
flex-wrap: wrap;
gap: 2px 0;
margin-top: 8px;
color: #a1a1aa;
font-size: 12px;
line-height: 18px;
}
.map-preview-summary > span + span::before {
margin-inline: 6px;
color: #52525b;
content: '·';
}
.map-preview-runtime-status {
font-weight: 600;
}
.legacy-logout-button:hover,
.legacy-logout-button:focus,
.legacy-logout-button:active {
@@ -14,6 +14,8 @@ const submitting = ref(false);
const errorMessage = ref('');
const infoMessage = ref('');
const oauthSessionId = ref('');
const passwordSetupSessionId = ref('');
const passwordSetupSuccessStatus = ref<'login' | 'verified'>('login');
const email = ref('');
const username = ref('');
const password = ref('');
@@ -60,6 +62,12 @@ const completeExchange = async (): Promise<void> => {
infoMessage.value = '카카오톡으로 임시 비밀번호를 보냈습니다.';
return;
}
if (result.status === 'password_setup') {
passwordSetupSessionId.value = result.oauthSessionId;
passwordSetupSuccessStatus.value = result.successStatus;
email.value = result.email;
return;
}
if (result.status === 'account_recovery') {
accountRecovery.value = result;
email.value = result.email;
@@ -94,6 +102,12 @@ const resolveAccount = async (): Promise<void> => {
await router.replace('/lobby');
return;
}
if (result.status === 'password_setup') {
passwordSetupSessionId.value = result.oauthSessionId;
passwordSetupSuccessStatus.value = result.successStatus;
email.value = result.email;
return;
}
oauthSessionId.value = result.oauthSessionId;
email.value = result.email;
} catch (error) {
@@ -103,6 +117,36 @@ const resolveAccount = async (): Promise<void> => {
}
};
const setMigratedPassword = async (): Promise<void> => {
errorMessage.value = '';
if (password.value !== confirmPassword.value) {
errorMessage.value = '비밀번호 확인이 일치하지 않습니다.';
return;
}
submitting.value = true;
try {
const credential = await sealPassword(password.value);
const result = await trpc.auth.kakaoSetPassword.mutate({
oauthSessionId: passwordSetupSessionId.value,
credential,
});
password.value = '';
confirmPassword.value = '';
passwordSetupSessionId.value = '';
if (result.status === 'otp') {
otpChallenge.value = result;
otpSuccessStatus.value = passwordSetupSuccessStatus.value;
return;
}
window.localStorage.setItem('sammo-session-token', result.sessionToken);
await router.replace(result.status === 'verified' ? '/lobby?verified=1' : '/lobby');
} catch (error) {
errorMessage.value = error instanceof Error ? error.message : '새 비밀번호를 설정하지 못했습니다.';
} finally {
submitting.value = false;
}
};
const register = async (): Promise<void> => {
errorMessage.value = '';
if (password.value !== confirmPassword.value) {
@@ -156,7 +200,15 @@ onMounted(() => {
<main id="oauth-container">
<h1>삼국지 모의전투 HiDCHe</h1>
<section class="oauth-card">
<h2>{{ accountRecovery ? '카카오 계정 연결 확인' : '회원가입' }}</h2>
<h2>
{{
accountRecovery
? '카카오 계정 연결 확인'
: passwordSetupSessionId
? '새 비밀번호 설정'
: '회원가입'
}}
</h2>
<p v-if="loading" class="oauth-message">카카오 인증을 확인하는 중...</p>
<p v-else-if="infoMessage" class="oauth-message" role="status">{{ infoMessage }}</p>
<div v-else-if="accountRecovery" class="recovery-panel" role="group" aria-label="카카오 계정 연결 확인">
@@ -181,6 +233,46 @@ onMounted(() => {
<RouterLink class="back-link" to="/">취소</RouterLink>
</div>
</div>
<form
v-else-if="passwordSetupSessionId"
class="password-setup-form"
aria-label=" 비밀번호 설정"
@submit.prevent="setMigratedPassword"
>
<p class="oauth-message">
카카오 인증으로 기존 계정을 확인했습니다. 앞으로 사용할 비밀번호를 설정해 주세요.
</p>
<div class="form-row">
<label for="migrated-password-email">카카오 이메일</label>
<input id="migrated-password-email" :value="email" readonly />
</div>
<div class="form-row">
<label for="migrated-password"> 비밀번호</label>
<input
id="migrated-password"
v-model="password"
type="password"
minlength="6"
autocomplete="new-password"
required
/>
</div>
<div class="form-row">
<label for="migrated-password-confirm">비밀번호 확인</label>
<input
id="migrated-password-confirm"
v-model="confirmPassword"
type="password"
minlength="6"
autocomplete="new-password"
required
/>
</div>
<button class="register-button" type="submit" :disabled="submitting">
{{ submitting ? '설정 중...' : '새 비밀번호 설정' }}
</button>
<RouterLink class="back-link" to="/">취소</RouterLink>
</form>
<form v-else-if="oauthSessionId" @submit.prevent="register">
<div class="form-row">
<label for="oauth-email">카카오 이메일</label>