diff --git a/.gitignore b/.gitignore index 553a01f..dfd9e6b 100644 --- a/.gitignore +++ b/.gitignore @@ -84,3 +84,5 @@ docs/* .env.example caquick_ddl.sql src/features/example/* + +.figma/ diff --git a/src/features/user/constants/user.constants.ts b/src/features/user/constants/user.constants.ts index db7fac5..5c64aa7 100644 --- a/src/features/user/constants/user.constants.ts +++ b/src/features/user/constants/user.constants.ts @@ -8,6 +8,13 @@ export const MAX_NICKNAME_LENGTH = 20; export const MIN_PHONE_LENGTH = 7; export const MAX_PHONE_LENGTH = 20; +// ── 생년월일 ── + +// figma 명세 외 정책 결정: 1900-01-01 이전 입력은 거부 (사실상 봇/오입력 방지). +// GraphQL DateTime은 ISO string을 UTC로 해석하므로 비교 기준도 UTC 자정으로 둔다. +// 운영 timezone과 무관하게 동일하게 동작. +export const MIN_BIRTH_DATE = new Date(Date.UTC(1900, 0, 1)); + // ── 페이지네이션 ── export const DEFAULT_PAGINATION_LIMIT = 20; diff --git a/src/features/user/services/user-base.service.spec.ts b/src/features/user/services/user-base.service.spec.ts index ce4bd08..6590cd0 100644 --- a/src/features/user/services/user-base.service.spec.ts +++ b/src/features/user/services/user-base.service.spec.ts @@ -222,21 +222,41 @@ describe('UserBaseService (real DB)', () => { ); }); + it('1899-12-31 등 1900-01-01 이전이면 BadRequestException을 던진다', () => { + expect(() => service.testNormalizeBirthDate('1899-12-31')).toThrow( + BadRequestException, + ); + expect(() => service.testNormalizeBirthDate('1850-01-01')).toThrow( + BadRequestException, + ); + }); + + it('1900-01-01은 통과한다 (UTC 기준)', () => { + const result = service.testNormalizeBirthDate('1900-01-01'); + expect(result).toBeInstanceOf(Date); + expect(result?.getUTCFullYear()).toBe(1900); + expect(result?.getUTCMonth()).toBe(0); + expect(result?.getUTCDate()).toBe(1); + }); + it('문자열 날짜를 Date 객체로 변환한다', () => { const result = service.testNormalizeBirthDate('1990-05-15'); expect(result).toBeInstanceOf(Date); - expect(result?.getFullYear()).toBe(1990); + expect(result?.getUTCFullYear()).toBe(1990); + expect(result?.getUTCMonth()).toBe(4); + expect(result?.getUTCDate()).toBe(15); }); - it('오늘 날짜는 미래로 취급하지 않고 그대로 반환한다', () => { - const today = new Date(); - today.setHours(12, 0, 0, 0); + it('오늘(UTC) 날짜는 미래로 취급하지 않고 그대로 반환한다', () => { + const now = new Date(); + const todayIso = `${now.getUTCFullYear()}-${String(now.getUTCMonth() + 1).padStart(2, '0')}-${String(now.getUTCDate()).padStart(2, '0')}`; - const result = service.testNormalizeBirthDate(today); + const result = service.testNormalizeBirthDate(todayIso); expect(result).toBeInstanceOf(Date); - // 시간은 00:00:00으로 내부 정규화되지만 날짜 자체는 오늘과 같아야 함 - expect(result?.toDateString()).toBe(today.toDateString()); + expect(result?.getUTCFullYear()).toBe(now.getUTCFullYear()); + expect(result?.getUTCMonth()).toBe(now.getUTCMonth()); + expect(result?.getUTCDate()).toBe(now.getUTCDate()); }); }); diff --git a/src/features/user/services/user-base.service.ts b/src/features/user/services/user-base.service.ts index 9aeab63..920512b 100644 --- a/src/features/user/services/user-base.service.ts +++ b/src/features/user/services/user-base.service.ts @@ -10,6 +10,7 @@ import { MAX_NICKNAME_LENGTH, MAX_PAGINATION_LIMIT, MAX_PHONE_LENGTH, + MIN_BIRTH_DATE, MIN_NICKNAME_LENGTH, MIN_PHONE_LENGTH, } from '@/features/user/constants/user.constants'; @@ -107,11 +108,21 @@ export abstract class UserBaseService { if (Number.isNaN(date.getTime())) { throw new BadRequestException('Invalid birthDate.'); } - const today = new Date(); - today.setHours(0, 0, 0, 0); - const normalized = new Date(date); - normalized.setHours(0, 0, 0, 0); - if (normalized > today) { + // DB가 @db.Date(시간 무시) + GraphQL DateTime이 ISO string을 UTC로 해석하므로 + // timezone 독립적으로 UTC 자정 기준으로 정규화한다. + const normalized = new Date( + Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()), + ); + if (normalized < MIN_BIRTH_DATE) { + throw new BadRequestException( + 'birthDate is too old (before 1900-01-01).', + ); + } + const now = new Date(); + const todayUtc = new Date( + Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()), + ); + if (normalized > todayUtc) { throw new BadRequestException('birthDate cannot be in the future.'); } return normalized;