diff --git a/docs/history.md b/docs/history.md index c4f3ed0..e867afc 100644 --- a/docs/history.md +++ b/docs/history.md @@ -1,5 +1,9 @@ # 의사결정 이력 +## 2026-04-02 v1.4.16 +- 백엔드/DB 장애 상황을 단순 연결 실패처럼 보여주면 사용자가 원인을 잘못 이해하게 되므로, 네트워크 단절과 서버 점검/초기화 실패를 전역 UI에서 분리해서 안내하는 편이 맞다고 판단했다. +- 이런 장애 안내는 각 화면별 에러 문구를 따로 손보는 것보다 `api` 공통 계층에서 상태를 감지하고 `App` 셸이 한 번에 전환하는 구조가 재사용성과 유지보수 측면에서 더 안전하다고 정리했다. + ## 2026-04-02 v1.4.15 - 실제 운영 DB에서 마지막 500 원인을 먼저 재현해본 결과, 스키마 설계보다 MariaDB의 `SHOW ... LIKE ?` 플레이스홀더 비호환과 부분 마이그레이션 상태 재진입 이슈가 핵심이었으므로, 이 단계에선 구조 변경보다 기동 안정성을 먼저 회복하는 편이 맞다고 판단했다. - 마이그레이션 로직은 “처음 실행”뿐 아니라 “반쯤 적용된 상태에서 다시 실행”도 견뎌야 하므로, 컬럼 존재 확인과 조건 분기를 모두 공용 `information_schema` 검사로 모으는 편이 더 안전하다고 정리했다. diff --git a/docs/todo.md b/docs/todo.md index 5390b68..64fbcd0 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -1,6 +1,7 @@ # 할 일 및 이슈 ## 단기 확인 +- `v1.4.16`에서 장애 전용 안내 화면을 붙였으므로, 실제로 `db_init_failed`와 네트워크 차단 상황에서 각각 `서비스 점검 중`, `서버 연결 확인 중` 화면이 기대대로 분기되는지 한 번 더 QA한다. - `v1.4.15`에서 `ensureData()`가 실제 운영 DB 설정으로 `ok`까지 통과한 것은 확인했으므로, 이제는 브라우저에서 `/api/auth/me`, `/api/auth/meta`, `/api/topics` 500이 실제로 사라졌는지와 기존 세션 로그인 흐름이 복구됐는지 한 번 더 QA한다. - `v1.4.14`부터는 DB 마이그레이션이 rename 대신 복사 기반으로 바뀌었으므로, 실제 운영 DB에서 서버 재시작 후 `topics` 계열 테이블과 `tierlists.topic_id`, `template_requests.source_topic_id/target_topic_id`가 기대대로 채워지는지 먼저 확인한다. - 레거시 `/games/...`와 `/editor/:gameId/...`는 redirect로 남겼으므로, 오래된 북마크 진입 후 주소가 `/topics/...`, `/editor/:topicId/...`로 자연스럽게 정규화되는지 한 번 더 QA한다. diff --git a/docs/update.md b/docs/update.md index a0d113a..0c8229b 100644 --- a/docs/update.md +++ b/docs/update.md @@ -1,5 +1,9 @@ # 업데이트 로그 +## 2026-04-02 v1.4.16 +- 백엔드나 DB 장애가 났을 때 일반 화면에서 계속 `연결할 수 없어요` 식으로 보이던 흐름을 정리하고, `api` 공통 요청 계층에서 `db_init_failed` 같은 500과 네트워크 실패를 감지해 앱 전체를 점검/연결 확인 화면으로 전환하도록 바꿨다. +- 이제 데이터베이스 초기화 실패나 서버 내부 500은 `서비스 점검 중`, 네트워크 단절은 `서버 연결 확인 중`으로 구분되어 보이며, 사용자는 일반 페이지 대신 전용 안내 화면과 다시 시도 버튼을 보게 된다. + ## 2026-04-02 v1.4.15 - `db_init_failed`의 직접 원인은 MariaDB에서 `SHOW TABLES LIKE ?`, `SHOW COLUMNS ... LIKE ?` 플레이스홀더를 허용하지 않던 부분이었고, 이를 `information_schema` 조회 기반으로 바꿔 실제 운영 DB에서도 `ensureData()`가 정상 통과되게 고쳤다. - 중간 마이그레이션 상태에서 `template_requests.target_topic_id`가 이미 생긴 DB는 중복 컬럼 추가로 다시 실패할 수 있었으므로, 해당 확인도 `columnExists()` 기준으로 바꿔 부분 적용된 DB까지 안전하게 다시 기동되게 정리했다. diff --git a/frontend/src/App.vue b/frontend/src/App.vue index b3ef536..2c0c0b1 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -34,6 +34,8 @@ const isGuideModalOpen = ref(false) const themeMode = ref('dark') const guideStepIndex = ref(0) const viewportWidth = ref(typeof window !== 'undefined' ? window.innerWidth : 1440) +const backendState = ref('online') +const backendMessage = ref('') provide('rightRailOpen', rightRailOpen) provide('localRightRailTarget', '#local-right-rail-root') @@ -138,6 +140,7 @@ const themeToggleLabel = computed(() => (isLightTheme.value ? '다크 모드' : const showSettingsThemePanel = computed(() => false && route.name === 'profile') const showTopicViewToggle = computed(() => route.name === 'topicHub') const topicViewMode = computed(() => (route.query.view === 'list' ? 'list' : 'grid')) +const showBackendFallback = computed(() => !isPreviewMode.value && ['maintenance', 'offline'].includes(backendState.value)) const leftBottomPrimaryAction = computed(() => { if (!authReady.value) return null if (route.name === 'home' && auth.user) { @@ -251,6 +254,13 @@ function syncViewportWidth() { viewportWidth.value = window.innerWidth } +function handleBackendStatus(event) { + const state = event?.detail?.state + if (!state) return + backendState.value = state + backendMessage.value = typeof event?.detail?.message === 'string' ? event.detail.message : '' +} + function applyTheme(mode) { themeMode.value = mode === 'light' ? 'light' : 'dark' if (typeof document !== 'undefined') document.documentElement.dataset.theme = themeMode.value @@ -270,6 +280,7 @@ onMounted(async () => { await auth.refresh() if (typeof window !== 'undefined') { syncViewportWidth() + window.addEventListener('tier-maker:backend-status', handleBackendStatus) window.addEventListener('resize', syncViewportWidth) window.addEventListener('keydown', handleGlobalKeydown) const leftSaved = window.localStorage.getItem('tier-maker:left-rail-collapsed') @@ -292,6 +303,7 @@ function handleGlobalKeydown(event) { onBeforeUnmount(() => { if (typeof window !== 'undefined') { + window.removeEventListener('tier-maker:backend-status', handleBackendStatus) window.removeEventListener('resize', syncViewportWidth) window.removeEventListener('keydown', handleGlobalKeydown) } @@ -400,6 +412,11 @@ function submitGlobalSearch() { router.push(homePath(query)) } +function reloadApp() { + if (typeof window === 'undefined') return + window.location.reload() +} + @@ -414,7 +431,26 @@ function submitGlobalSearch() { }" :style="shellStyle" > -