Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ec9a334035 | |||
| 39a731138b |
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
- 프로젝트명: 10 Minute Planner 웹 UI
|
- 프로젝트명: 10 Minute Planner 웹 UI
|
||||||
- 기술 스택: Vue 3 + Vite + TailwindCSS + JavaScript
|
- 기술 스택: Vue 3 + Vite + TailwindCSS + JavaScript
|
||||||
- 현재 기준 버전: `v0.1.48` 준비 중
|
- 현재 기준 버전: `v0.1.50` 준비 중
|
||||||
- Git 원격 저장소: `https://git.sori.studio/zenn/planner.sori.studio.git`
|
- Git 원격 저장소: `https://git.sori.studio/zenn/planner.sori.studio.git`
|
||||||
|
|
||||||
## 기준 디자인
|
## 기준 디자인
|
||||||
@@ -230,6 +230,8 @@
|
|||||||
- 비밀번호 재설정/이메일 인증용 개발 링크는 `AUTH_PREVIEW_LINKS=true`여도 `APP_BASE_URL`이 `localhost` 또는 `127.0.0.1`일 때만 응답에 포함한다. 상용 서버에서 링크가 그대로 보이면 이 조건부터 확인한다.
|
- 비밀번호 재설정/이메일 인증용 개발 링크는 `AUTH_PREVIEW_LINKS=true`여도 `APP_BASE_URL`이 `localhost` 또는 `127.0.0.1`일 때만 응답에 포함한다. 상용 서버에서 링크가 그대로 보이면 이 조건부터 확인한다.
|
||||||
- 프론트는 `/verify-email?token=...` 진입 시 인증을 바로 확정하고 로그인 모달에 결과 메시지를 띄운다. `/reset-password?token=...`은 기존처럼 비밀번호 재설정 모달을 연다.
|
- 프론트는 `/verify-email?token=...` 진입 시 인증을 바로 확정하고 로그인 모달에 결과 메시지를 띄운다. `/reset-password?token=...`은 기존처럼 비밀번호 재설정 모달을 연다.
|
||||||
- 로그인 모달에는 `이메일 인증 메일 다시 보내기` 버튼이 추가되었다. 이메일 주소를 입력한 상태에서 바로 인증 메일 재전송을 요청할 수 있어, 가입 후 메일을 못 받은 사용자가 로그인 단계에서 막히지 않게 했다.
|
- 로그인 모달에는 `이메일 인증 메일 다시 보내기` 버튼이 추가되었다. 이메일 주소를 입력한 상태에서 바로 인증 메일 재전송을 요청할 수 있어, 가입 후 메일을 못 받은 사용자가 로그인 단계에서 막히지 않게 했다.
|
||||||
|
- 인증 메일 재전송 버튼은 항상 보이지 않는다. 로그인 모달에서 이메일 인증 관련 안내 메시지가 보일 때만 메시지 박스 오른쪽에 함께 노출한다.
|
||||||
|
- 인증 메일 재전송 버튼은 현재 `이메일 인증을 완료한 뒤 로그인해 주세요.` 안내가 있을 때만 표시한다. 인증 완료 메시지나 일반 안내 문구에서는 보이지 않도록 조건을 좁혔다.
|
||||||
- SETTINGS 화면에 일반 사용자 전용 `회원 탈퇴` 카드가 추가되었다. 현재 비밀번호 확인 후 계정, 플래너 기록, 목표, 세션, 인증 토큰이 함께 삭제된다. 기본 관리자 계정은 이 경로에서 삭제하지 못하게 막는다.
|
- SETTINGS 화면에 일반 사용자 전용 `회원 탈퇴` 카드가 추가되었다. 현재 비밀번호 확인 후 계정, 플래너 기록, 목표, 세션, 인증 토큰이 함께 삭제된다. 기본 관리자 계정은 이 경로에서 삭제하지 못하게 막는다.
|
||||||
- `/api/auth/logout`이 추가되어 로그아웃 시 프론트 저장 토큰만 지우는 것이 아니라 서버 세션도 함께 폐기한다.
|
- `/api/auth/logout`이 추가되어 로그아웃 시 프론트 저장 토큰만 지우는 것이 아니라 서버 세션도 함께 폐기한다.
|
||||||
- SETTINGS 화면 왼쪽 카드에 현재 기기 로그인 유지 방식(`로그인 유지` 또는 브라우저 세션만 유지), 최근 로그인 시각, 이메일 인증 상태를 보여준다.
|
- SETTINGS 화면 왼쪽 카드에 현재 기기 로그인 유지 방식(`로그인 유지` 또는 브라우저 세션만 유지), 최근 로그인 시각, 이메일 인증 상태를 보여준다.
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "ten-minute-planner",
|
"name": "ten-minute-planner",
|
||||||
"version": "0.1.48",
|
"version": "0.1.50",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ten-minute-planner",
|
"name": "ten-minute-planner",
|
||||||
"version": "0.1.48",
|
"version": "0.1.50",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"vue": "^3.5.13"
|
"vue": "^3.5.13"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "ten-minute-planner",
|
"name": "ten-minute-planner",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.48",
|
"version": "0.1.50",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
16
src/App.vue
16
src/App.vue
@@ -11,6 +11,7 @@ import StatsDashboard from './components/StatsDashboard.vue'
|
|||||||
import {
|
import {
|
||||||
deleteAdminUser,
|
deleteAdminUser,
|
||||||
fetchAdminOverview,
|
fetchAdminOverview,
|
||||||
|
fetchAdminUserDetail,
|
||||||
revokeAdminUserSessions,
|
revokeAdminUserSessions,
|
||||||
updateAdminUserStatus,
|
updateAdminUserStatus,
|
||||||
} from './lib/adminApi'
|
} from './lib/adminApi'
|
||||||
@@ -114,6 +115,7 @@ const hiddenGuideTooltips = ref(readHiddenGuideTooltips())
|
|||||||
const ddayDisabledDateKeys = ref(readDdayDisabledDateKeys())
|
const ddayDisabledDateKeys = ref(readDdayDisabledDateKeys())
|
||||||
const adminBusy = ref(false)
|
const adminBusy = ref(false)
|
||||||
const adminActionUserId = ref(null)
|
const adminActionUserId = ref(null)
|
||||||
|
const adminDetailBusy = ref(false)
|
||||||
const adminMessage = ref('')
|
const adminMessage = ref('')
|
||||||
const adminOverview = ref({
|
const adminOverview = ref({
|
||||||
totalUsers: 0,
|
totalUsers: 0,
|
||||||
@@ -127,6 +129,8 @@ const adminOverview = ref({
|
|||||||
})
|
})
|
||||||
const adminUsers = ref([])
|
const adminUsers = ref([])
|
||||||
const adminRecentLogins = ref([])
|
const adminRecentLogins = ref([])
|
||||||
|
const adminSelectedUserId = ref(null)
|
||||||
|
const adminUserDetail = ref(null)
|
||||||
|
|
||||||
const hours = [
|
const hours = [
|
||||||
'6', '7', '8', '9', '10', '11', '12',
|
'6', '7', '8', '9', '10', '11', '12',
|
||||||
@@ -644,6 +648,17 @@ const authSessionInfo = computed(() => ({
|
|||||||
? '이메일 인증 완료'
|
? '이메일 인증 완료'
|
||||||
: '이메일 인증 필요',
|
: '이메일 인증 필요',
|
||||||
}))
|
}))
|
||||||
|
const showVerificationResend = computed(() => {
|
||||||
|
if (authMode.value !== 'login') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!authForm.email.includes('@')) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return authMessage.value.includes('이메일 인증을 완료한 뒤 로그인해 주세요.')
|
||||||
|
})
|
||||||
const filteredGoals = computed(() => {
|
const filteredGoals = computed(() => {
|
||||||
const query = goalQuery.value.trim().toLowerCase()
|
const query = goalQuery.value.trim().toLowerCase()
|
||||||
return goals.value.filter((goal) => {
|
return goals.value.filter((goal) => {
|
||||||
@@ -3277,6 +3292,7 @@ onBeforeUnmount(() => {
|
|||||||
:form="authForm"
|
:form="authForm"
|
||||||
:busy="authBusy"
|
:busy="authBusy"
|
||||||
:message="authMessage"
|
:message="authMessage"
|
||||||
|
:show-resend-verification="showVerificationResend"
|
||||||
@close="closeAuthDialog"
|
@close="closeAuthDialog"
|
||||||
@submit="submitAuthForm"
|
@submit="submitAuthForm"
|
||||||
@resend-verification="resendVerificationEmail"
|
@resend-verification="resendVerificationEmail"
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ const props = defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
showResendVerification: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits([
|
const emit = defineEmits([
|
||||||
@@ -186,12 +190,25 @@ function getSubmitLabel(mode, busy) {
|
|||||||
<span class="text-xs font-bold tracking-[0.08em] text-stone-700">로그인 상태 유지</span>
|
<span class="text-xs font-bold tracking-[0.08em] text-stone-700">로그인 상태 유지</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<p
|
<div
|
||||||
v-if="message"
|
v-if="message"
|
||||||
class="rounded-2xl border border-stone-300 bg-white/80 px-4 py-3 text-sm font-semibold leading-6 text-stone-700"
|
class="rounded-2xl border border-stone-300 bg-white/80 px-4 py-3"
|
||||||
>
|
>
|
||||||
{{ message }}
|
<div class="flex items-start justify-between gap-4">
|
||||||
</p>
|
<p class="min-w-0 text-sm font-semibold leading-6 text-stone-700">
|
||||||
|
{{ message }}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
v-if="showResendVerification"
|
||||||
|
type="button"
|
||||||
|
class="shrink-0 rounded-full border border-stone-300 px-3 py-2 text-[10px] font-bold tracking-[0.14em] text-stone-600 transition hover:border-stone-500 hover:text-stone-900"
|
||||||
|
:disabled="busy"
|
||||||
|
@click="emit('resend-verification')"
|
||||||
|
>
|
||||||
|
재전송
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
@@ -211,16 +228,6 @@ function getSubmitLabel(mode, busy) {
|
|||||||
비밀번호를 잊으셨나요?
|
비밀번호를 잊으셨나요?
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
|
||||||
v-if="mode === 'login'"
|
|
||||||
type="button"
|
|
||||||
class="mt-3 w-full text-center text-xs font-bold tracking-[0.14em] text-stone-500 underline underline-offset-4 transition hover:text-stone-900"
|
|
||||||
:disabled="busy"
|
|
||||||
@click="emit('resend-verification')"
|
|
||||||
>
|
|
||||||
이메일 인증 메일 다시 보내기
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="mt-5 flex items-center justify-center gap-2 border-t border-stone-300/70 pt-4">
|
<div class="mt-5 flex items-center justify-center gap-2 border-t border-stone-300/70 pt-4">
|
||||||
<p class="text-sm font-semibold text-stone-600">
|
<p class="text-sm font-semibold text-stone-600">
|
||||||
{{ mode === 'signup' ? '이미 계정이 있나요?' : '계정 화면으로 돌아갈까요?' }}
|
{{ mode === 'signup' ? '이미 계정이 있나요?' : '계정 화면으로 돌아갈까요?' }}
|
||||||
|
|||||||
Reference in New Issue
Block a user