diff --git a/.env.example b/.env.example index a46e457..c428b15 100644 --- a/.env.example +++ b/.env.example @@ -10,3 +10,4 @@ APP_BASE_URL=https://planner.sori.studio RESEND_API_KEY=replace-with-private-resend-api-key MAIL_FROM_EMAIL=planner@sori.studio MAIL_FROM_NAME=10 Minute Planner +AUTH_PREVIEW_LINKS=false diff --git a/HANDOFF.md b/HANDOFF.md index ca97e32..e1f2bfa 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -222,6 +222,7 @@ - 왼쪽 사이드바의 인쇄 영역은 `PRINT` 버튼 하나로 줄이고, 클릭 시 모달에서 시작일/종료일과 `1페이지씩` 또는 `2페이지씩` 출력 방식을 선택한다. 인쇄 전용 렌더링은 선택 기간의 날짜를 순서대로 여러 장 생성하며, 2페이지씩 출력에서 홀수 날짜가 남으면 오른쪽은 빈 페이지 프레임으로 둔다. - 회원가입과 프로필 수정 시 이메일뿐 아니라 닉네임 중복도 서버에서 409로 막는다. 비밀번호 재설정 API는 로그인 모달의 `비밀번호 찾기` 흐름과 연결되어 있고, `/reset-password?token=...` URL로 들어오면 새 비밀번호 설정 모드가 열린다. 실제 메일 발송 전까지는 백엔드 응답의 `resetPreviewUrl`을 개발용 링크로 표시한다. - Resend 메일러가 추가되었다. `RESEND_API_KEY`, `MAIL_FROM_EMAIL`, `MAIL_FROM_NAME`, `APP_BASE_URL` 환경변수를 설정하면 이메일 인증과 비밀번호 재설정 메일을 실제로 발송한다. API 키는 저장소에 커밋하지 말고 루트 `.env`/`.env.dev`에만 넣는다. +- 인증/비밀번호 재설정 개발용 링크는 `AUTH_PREVIEW_LINKS=true`일 때만 API 응답에 포함된다. 운영 `.env`는 반드시 `AUTH_PREVIEW_LINKS=false`로 두어야 하며, 현재 예시 파일도 false가 기본값이다. - `5173` 포트가 직접 `npm run dev` 없이 열려 있으면 대부분 `ten-minute-frontend-dev` 컨테이너가 떠 있는 상태다. 개발용 Docker를 끄려면 `docker compose -f docker-compose.dev.yml down`을 사용한다. - 현재 환경에서는 Docker 데몬이 꺼져 있어서 `docker compose build` 실검증은 하지 못했고, 데몬 시작 후 다시 확인이 필요하다. - 이미지 저장 기능은 추후 `print-only` 또는 별도 export 전용 레이아웃을 기준으로 구현하면 화면/인쇄/공유 결과를 맞추기 쉽다. diff --git a/backend/src/config.js b/backend/src/config.js index 09867d5..b480a39 100644 --- a/backend/src/config.js +++ b/backend/src/config.js @@ -17,6 +17,7 @@ const envSchema = z.object({ RESEND_API_KEY: z.string().optional(), MAIL_FROM_EMAIL: z.string().email().default('planner@sori.studio'), MAIL_FROM_NAME: z.string().default('10 Minute Planner'), + AUTH_PREVIEW_LINKS: z.coerce.boolean().default(false), ADMIN_ACCOUNT_ID: z.string().min(1), ADMIN_ACCOUNT_PASSWORD: z.string().min(12), ADMIN_ACCOUNT_EMAIL: z.string().email(), diff --git a/backend/src/routes/auth.js b/backend/src/routes/auth.js index 1d2cabc..54f143a 100644 --- a/backend/src/routes/auth.js +++ b/backend/src/routes/auth.js @@ -109,6 +109,17 @@ function sanitizeUser(user) { } } +function withPreviewUrl(payload, key, previewUrl) { + if (!env.AUTH_PREVIEW_LINKS) { + return payload + } + + return { + ...payload, + [key]: previewUrl, + } +} + async function findUserByNickname(nickname) { const [user] = await db .select() @@ -177,12 +188,11 @@ export async function registerAuthRoutes(app) { linkUrl: verification.previewUrl, }) - return reply.code(201).send({ + return reply.code(201).send(withPreviewUrl({ message: '회원가입이 완료되었습니다.', token, user: sanitizeUser(user), - verificationPreviewUrl: verification.previewUrl, - }) + }, 'verificationPreviewUrl', verification.previewUrl)) }) app.post('/api/auth/login', async (request, reply) => { @@ -397,10 +407,9 @@ export async function registerAuthRoutes(app) { linkUrl: verification.previewUrl, }) - return { + return withPreviewUrl({ message: '이메일 인증 링크를 준비했습니다.', - verificationPreviewUrl: verification.previewUrl, - } + }, 'verificationPreviewUrl', verification.previewUrl) }) app.post('/api/auth/verification/confirm', async (request, reply) => { @@ -485,10 +494,9 @@ export async function registerAuthRoutes(app) { linkUrl: reset.previewUrl, }) - return { + return withPreviewUrl({ message: '비밀번호 재설정 링크를 준비했습니다.', - resetPreviewUrl: reset.previewUrl, - } + }, 'resetPreviewUrl', reset.previewUrl) }) app.post('/api/auth/password-reset/confirm', async (request, reply) => {