@@ -111,8 +70,15 @@
{{ greetingMessage }}
+
+
-
+
@@ -229,6 +195,11 @@ const FIRST_MISSION: CurrentMissionResponse = {
weeklyStatus: null,
popup: { exists: false, type: null },
}
+// 최고 레벨: 백엔드가 mission=null + currentLevel=10으로 내려줌 (진행할 미션 없음)
+const MAX_LEVEL = 10
+const isMaxLevel = computed(
+ () => mission.value != null && mission.value.mission == null && mission.value.currentLevel >= MAX_LEVEL,
+)
// 미션이 비어 있으면 기본(첫 회고) 카드로 대체 — 카드가 항상 레벨에 맞게 뜨도록
const displayMission = computed
(() =>
mission.value?.mission ? mission.value : FIRST_MISSION,
diff --git a/app/pages/index.vue b/app/pages/index.vue
index 69f6a24..a50d9cd 100644
--- a/app/pages/index.vue
+++ b/app/pages/index.vue
@@ -13,6 +13,12 @@ definePageMeta({ layout: false })
const { $api } = useNuxtApp()
const runtimeConfig = useRuntimeConfig()
+const push = usePushNotifications()
+
+// 인증·온보딩 완료 사용자의 최종 목적지 — 알림 탭 딥링크가 있으면 그쪽 우선, 없으면 홈
+function authedDestination(): string {
+ return push.consumePendingDeepLink() ?? '/home'
+}
onMounted(async () => {
// 이미 로그인된 경우 스플래시 없이 즉시 이동
@@ -23,7 +29,7 @@ onMounted(async () => {
navigateTo('/login', { replace: true })
return
}
- navigateTo(isOnboardingCompleted === 'true' ? '/home' : '/onboarding', { replace: true })
+ navigateTo(isOnboardingCompleted === 'true' ? authedDestination() : '/onboarding', { replace: true })
return
}
@@ -75,7 +81,7 @@ onMounted(async () => {
navigateTo('/login', { replace: true })
return
}
- navigateTo(isOnboardingCompleted === 'true' ? '/home' : '/onboarding', { replace: true })
+ navigateTo(isOnboardingCompleted === 'true' ? authedDestination() : '/onboarding', { replace: true })
} catch {
navigateTo('/login', { replace: true })
}
diff --git a/app/pages/login.vue b/app/pages/login.vue
index 499a7f4..1130581 100644
--- a/app/pages/login.vue
+++ b/app/pages/login.vue
@@ -112,6 +112,8 @@ const isLoading = ref(false)
// 로그인 실패·안내 토스트는 notice 아이콘 표시 (피그마 8088:31255)
const { show } = useToast()
const showToast = (msg: string) => show(msg, { icon: true })
+// 로그인 성공 직후 푸시 기기 토큰을 등록하기 위해 사용
+const push = usePushNotifications()
onMounted(async () => {
// accessToken과 refreshToken이 모두 있을 때만 자동 로그인
@@ -177,6 +179,11 @@ async function submitLogin(provider: 'KAKAO' | 'GOOGLE' | 'APPLE', oauthToken: s
const { data } = await $api.post>('/api/v1/auth/login', { provider, oauthToken })
localStorage.setItem('accessToken', data.data.accessToken)
localStorage.setItem('refreshToken', data.data.refreshToken)
+ // 온보딩 완료 여부를 저장해야 다음 앱 실행 시 스플래시(index.vue)가 세션을 유지한다.
+ // (이 값이 없으면 스플래시가 미로그인으로 간주해 localStorage를 비우고 로그인 화면으로 보냄 → 매번 재로그인)
+ localStorage.setItem('isOnboardingCompleted', String(data.data.isOnboardingCompleted))
+ // 로그인 직후 푸시 동의 상태면 기기 토큰을 즉시 등록한다 (앱 재실행 전까지 알림이 누락되던 문제 방지)
+ push.syncIfConsented()
identify(data.data.accessToken, { provider: provider.toLowerCase() })
diff --git a/app/pages/my/index.vue b/app/pages/my/index.vue
index 553d698..e5207d5 100644
--- a/app/pages/my/index.vue
+++ b/app/pages/my/index.vue
@@ -13,8 +13,9 @@
{{ jobLabel }}
{{ profile?.nickname ?? '' }}
-
+
Lv.{{ displayLevel }}
@@ -142,8 +143,10 @@ const jobLabels: Record
= {
const jobLabel = computed(() => (profile.value?.job ? jobLabels[profile.value.job] : ''))
-// 백엔드 currentLevel은 '달성한 레벨'이라 신규 유저는 0 — 표시 레벨은 최소 1 (모든 유저 Lv.1부터)
-const displayLevel = computed(() => Math.max(profile.value?.currentLevel ?? 1, 1))
+// 백엔드 currentLevel은 '달성한 레벨'이라 신규·회고X 유저는 0
+// → 레벨 1 이상(회고 O)부터 배지 노출, 그 전엔 배지 자체를 숨김
+const showLevelBadge = computed(() => (profile.value?.currentLevel ?? 0) >= 1)
+const displayLevel = computed(() => profile.value?.currentLevel ?? 1)
const { badges, load: loadBadges } = useBadges()
const acquiredBadges = computed(() =>
diff --git a/app/pages/retrospect/start.vue b/app/pages/retrospect/start.vue
index 0775609..2dea536 100644
--- a/app/pages/retrospect/start.vue
+++ b/app/pages/retrospect/start.vue
@@ -311,7 +311,9 @@ const hasAnswered = computed(() => messages.value.some((m) => m.role === 'user')
const voiceSupported = computed(
() => isNative.value || (import.meta.client && !!navigator.mediaDevices?.getUserMedia),
)
-const showVoice = computed(() => voiceSupported.value && inputText.value.trim().length === 0)
+// 한글 IME 조합 중에는 v-model(inputText)이 갱신되지 않아, DOM 값을 직접 추적해 즉시 반영
+const hasInput = ref(false)
+const showVoice = computed(() => voiceSupported.value && !hasInput.value)
const exitDescription = computed(() =>
hasAnswered.value
? '지금까지 작성한 내용은 저장되지 않으며, 오늘 회고 횟수 1회가 차감돼요.'
@@ -357,6 +359,8 @@ function autoGrow() {
el.style.height = 'auto'
el.style.height = `${Math.min(el.scrollHeight, 120)}px`
isMultiline.value = el.scrollHeight > 40
+ // 조합 중 글자를 포함해 실제 입력 유무 판단 (IME 대응)
+ hasInput.value = el.value.trim().length > 0
}
// 질문 순번 계산: questionType('Q1'..)에서 숫자 추출, 없으면 카운터 증가
@@ -642,6 +646,7 @@ async function onConfirmRestart() {
retrospectiveId.value = res.retrospectiveId
messages.value = []
inputText.value = ''
+ hasInput.value = false
questionNo.value = 0
deepAsked.value = false
lastDeepId.value = null
diff --git a/app/stores/auth.ts b/app/stores/auth.ts
index 0334020..c711887 100644
--- a/app/stores/auth.ts
+++ b/app/stores/auth.ts
@@ -5,19 +5,17 @@ export const useAuthStore = defineStore('auth', () => {
const user = ref(null)
const isLoggedIn = computed(() => !!user.value)
- // useProfile()과 동일한 전역 캐시 키 — 로그아웃 시 함께 비워
- // 다른 계정으로 재로그인했을 때 이전 계정 프로필(닉네임 등)이 남는 것을 방지
- const profileCache = useState('user:profile', () => null)
- const profileInflight = useState | null>('user:profile:inflight', () => null)
-
function setUser(profile: UserProfile) {
user.value = profile
}
function logout() {
user.value = null
- profileCache.value = null
- profileInflight.value = null
+ // 로그아웃은 navigateTo로 SPA 이동이라 페이지가 리로드되지 않는다.
+ // → 전역 useState 캐시(프로필·프로젝트·회고·홈·알림 등)를 모두 비워
+ // 다른 계정으로 재로그인했을 때 이전 계정 데이터가 남는 것을 방지한다.
+ // (user 스코프가 아닌 키는 다음 접근 시 기본값 팩토리로 재생성됨)
+ clearNuxtState()
localStorage.removeItem('accessToken')
localStorage.removeItem('refreshToken')
}
diff --git a/app/types/api.ts b/app/types/api.ts
index 3c75407..b24c50c 100644
--- a/app/types/api.ts
+++ b/app/types/api.ts
@@ -131,14 +131,11 @@ export interface RecentRetrospective {
// Mission / Level (게이미피케이션) — GET /api/v1/missions/current
export type MissionType = 'FIRST_RETRO' | 'TIME_LIMITED' | 'CONSECUTIVE_WEEK' | 'CUMULATIVE_RETRO'
-// 연속 주 미션에서만 내려옴 (월~일 7요소)
-export interface WeekDayStatus {
- day: string
- isCompleted: boolean
-}
-
+// 연속 주 미션에서만 내려옴. 백엔드 WeeklyStatus 계약: days는 이번 주 월~일 7개 boolean
export interface WeeklyRetroStatus {
- weekDays: WeekDayStatus[]
+ show: boolean
+ weekStart: string
+ days: boolean[]
}
// 미션 상세 (최고 레벨 등에서는 mission = null)
diff --git a/package.json b/package.json
index d807876..5f276cb 100644
--- a/package.json
+++ b/package.json
@@ -9,6 +9,9 @@
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
+ "dev:host": "nuxt dev --host 0.0.0.0",
+ "cap:live:android": "cap run android --live-reload --external --port 3000",
+ "cap:live:ios": "cap run ios --live-reload --external --port 3000",
"generate": "nuxt generate",
"cap:sync": "nuxt generate && cap sync",
"cap:sync:prod": "nuxt generate --dotenv .env.production && cap sync",