diff --git a/HANDOFF.md b/HANDOFF.md index b5968bd..1c3d746 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -4,7 +4,7 @@ - 프로젝트명: 10 Minute Planner 웹 UI - 기술 스택: Vue 3 + Vite + TailwindCSS + JavaScript -- 현재 기준 버전: `v0.1.47` 준비 중 +- 현재 기준 버전: `v0.1.48` 준비 중 - Git 원격 저장소: `https://git.sori.studio/zenn/planner.sori.studio.git` ## 기준 디자인 @@ -229,6 +229,7 @@ - 기존에 발급된 세션이라도 일반 사용자 이메일 인증이 안 되어 있으면 `/api/auth/me` 단계에서 세션을 즉시 폐기한다. 운영 중 인증 정책을 켠 뒤에도 미인증 세션이 남지 않게 하기 위한 장치다. - 비밀번호 재설정/이메일 인증용 개발 링크는 `AUTH_PREVIEW_LINKS=true`여도 `APP_BASE_URL`이 `localhost` 또는 `127.0.0.1`일 때만 응답에 포함한다. 상용 서버에서 링크가 그대로 보이면 이 조건부터 확인한다. - 프론트는 `/verify-email?token=...` 진입 시 인증을 바로 확정하고 로그인 모달에 결과 메시지를 띄운다. `/reset-password?token=...`은 기존처럼 비밀번호 재설정 모달을 연다. +- 로그인 모달에는 `이메일 인증 메일 다시 보내기` 버튼이 추가되었다. 이메일 주소를 입력한 상태에서 바로 인증 메일 재전송을 요청할 수 있어, 가입 후 메일을 못 받은 사용자가 로그인 단계에서 막히지 않게 했다. - SETTINGS 화면에 일반 사용자 전용 `회원 탈퇴` 카드가 추가되었다. 현재 비밀번호 확인 후 계정, 플래너 기록, 목표, 세션, 인증 토큰이 함께 삭제된다. 기본 관리자 계정은 이 경로에서 삭제하지 못하게 막는다. - `/api/auth/logout`이 추가되어 로그아웃 시 프론트 저장 토큰만 지우는 것이 아니라 서버 세션도 함께 폐기한다. - SETTINGS 화면 왼쪽 카드에 현재 기기 로그인 유지 방식(`로그인 유지` 또는 브라우저 세션만 유지), 최근 로그인 시각, 이메일 인증 상태를 보여준다. diff --git a/TODO.md b/TODO.md index d0c53b0..d2894cf 100644 --- a/TODO.md +++ b/TODO.md @@ -104,35 +104,3 @@ - [x] 관리자 페이지에서 계정 비활성화 / 강제 로그아웃 / 삭제 기능을 추가한다. - [ ] 관리자 페이지에서 사용자별 문서 상세 조회 기능을 추가한다. - [ ] 관리자 페이지에서 검색 / 정렬 / 필터 UX를 추가한다. - -## 메모 - -- D-DAY는 본문에 직접 입력하는 방식보다, 별도 목표 목록에서 선택한 대표 목표를 보여주는 구조가 더 적합하다. -- 목표가 없는 경우 본문 D-DAY 영역은 숨기고, 오른쪽 패널의 `D-DAY 사용` 메뉴에서 검색/선택하도록 유도한다. -- `TIME TABLE` 드래그는 단순 사각형 선택이 아니라 시간 셀 단위의 연속 선택으로 해석한다. -- 로컬 저장은 비로그인/데모 보조 용도로만 남기고, 로그인 상태에서는 사용자별 서버 저장을 우선한다. -- 최종적으로는 회원 가입 후 각자 자신의 문서를 작성/관리하고, 개인 통계를 확인하며, 특정 날짜 문서를 출력할 수 있어야 한다. -- 실제 인쇄는 HTML/CSS 기반 프린트 레이아웃으로 유지하고, 공유용으로는 별도의 이미지 저장 기능을 추가하는 방향이 적합하다. -- 최종 배포는 UGREEN NAS에서 Docker 기반으로 동작할 예정이며, 포트와 실제 서비스 구성은 추후 확정한다. -- 백엔드는 빠른 목업이면 PocketBase도 가능하지만, 현재 방향상 커스텀 로직과 확장성을 생각하면 전용 Node.js API + DB 조합을 우선 검토한다. -- 현재 백엔드는 `backend/` 폴더에 `Fastify + Drizzle + PostgreSQL` 기준으로 전환 중이다. -- 현재 백엔드는 회원가입, 로그인, 현재 사용자 확인용 기본 인증 API까지 포함한다. -- 현재 백엔드는 사용자별 플래너 단건 저장/조회와 범위 조회 API까지 포함한다. -- 현재 백엔드는 사용자별 목표 목록 조회와 목표 생성 API까지 포함한다. -- 현재는 `docker-compose.yml`로 `postgres + backend + frontend(nginx)` 초안을 올릴 수 있게 정리했다. -- 현재 환경에서는 Docker 데몬이 꺼져 있어 `docker compose build` 실검증은 아직 완료하지 못했다. -- 프론트에는 로그인/회원가입 모달과 현재 사용자 상태 표시가 추가되었다. -- 로그인 상태일 때는 서버 저장을 우선 사용하는 흐름으로 전환 중이다. -- 로그인 전에는 플래너 본문을 사용하지 못하도록 막고, 인증 후 사용 흐름으로 정리했다. -- 현재는 각 날짜 플래너가 대표 목표 하나를 선택해 `D-DAY`에 연결하는 구조다. -- 목표는 별도 GOALS 화면에서 검색/생성/기간 설정을 관리하고, 플래너에서는 표시 ON/OFF만 다룬다. -- 목표가 현재 날짜에 적용되지 않았거나 `D-DAY 사용`이 꺼져 있으면 본문 `D-DAY` 영역은 숨긴다. -- 현재 날짜에 적용된 목표가 있으면 D-DAY는 기본적으로 보이고, 사용자가 해당 날짜에서만 OFF로 끌 수 있다. -- 목표 생성 시 표시 시작일 기본값은 오늘, 표시 종료일 기본값은 목표일로 맞춘다. -- 표시 기간이 다른 진행 중 목표와 겹치면 프론트와 백엔드 모두 저장을 막는다. -- 앱을 새로 열면 마지막 열람 날짜가 아니라 항상 오늘 날짜부터 시작하고, Docker/NAS 컨테이너 시간대는 `Asia/Seoul` 기준으로 맞춘다. -- TASK LABELS는 버튼 묶음 대신 동일한 토글 패턴으로 단순화했다. -- 구현할 때마다 완료된 항목은 체크하고, 큰 결정사항은 `HANDOFF.md`에도 함께 반영한다. -- 메일 발송은 현재 Resend 기준으로 운영한다. 무료 플랜/도메인 제약이 바뀌는 시점에만 별도 대체 수단을 다시 검토한다. -- 관리자 기능은 읽기 전용 대시보드부터 시작하고, 실제 정리 액션은 권한/감사 로그 정책을 정한 뒤 추가하는 편이 안전하다. -- 관리자 아이디/비밀번호는 README나 HANDOFF에 실제 값으로 남기지 않고, Docker 배포용 비공개 `.env`에서만 관리한다. diff --git a/package-lock.json b/package-lock.json index 91cb91a..80a33ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ten-minute-planner", - "version": "0.1.47", + "version": "0.1.48", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ten-minute-planner", - "version": "0.1.47", + "version": "0.1.48", "dependencies": { "vue": "^3.5.13" }, diff --git a/package.json b/package.json index 46ac087..2a82b98 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ten-minute-planner", "private": true, - "version": "0.1.47", + "version": "0.1.48", "type": "module", "scripts": { "dev": "vite", diff --git a/src/App.vue b/src/App.vue index 192d829..fe735d6 100644 --- a/src/App.vue +++ b/src/App.vue @@ -25,6 +25,7 @@ import { persistAuthState, readAuthState, requestPasswordReset, + requestVerification, signup, updatePassword, updateProfile, @@ -1635,6 +1636,28 @@ async function submitAuthForm() { } } +async function resendVerificationEmail() { + const email = authForm.email.trim() + + if (!email || !email.includes('@')) { + authMessage.value = '인증 메일을 다시 받으려면 이메일 주소를 먼저 입력해 주세요.' + return + } + + authBusy.value = true + + try { + const result = await requestVerification({ email }, authToken.value || undefined) + authMessage.value = result.verificationPreviewUrl + ? `${result.message} 개발용 링크: ${result.verificationPreviewUrl}` + : result.message + } catch (error) { + authMessage.value = toUserFacingApiError(error, '인증 메일을 다시 보내지 못했습니다.') + } finally { + authBusy.value = false + } +} + async function restoreAuthSession() { const savedAuth = readAuthState() @@ -3248,17 +3271,18 @@ onBeforeUnmount(() => { - +
+ +

{{ mode === 'signup' ? '이미 계정이 있나요?' : '계정 화면으로 돌아갈까요?' }} diff --git a/src/lib/authClient.js b/src/lib/authClient.js index a25d2f7..6785937 100644 --- a/src/lib/authClient.js +++ b/src/lib/authClient.js @@ -143,3 +143,11 @@ export async function confirmVerification({ token }) { body: { token }, }) } + +export async function requestVerification({ email }, token) { + return request('/api/auth/verification/request', { + method: 'POST', + token, + body: email ? { email } : {}, + }) +}