Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,5 @@ docs/*
.env.example
caquick_ddl.sql
src/features/example/*

.figma/
7 changes: 7 additions & 0 deletions src/features/user/constants/user.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
34 changes: 27 additions & 7 deletions src/features/user/services/user-base.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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());
});
});

Expand Down
21 changes: 16 additions & 5 deletions src/features/user/services/user-base.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Accept min birth date string in non-UTC timezones

When raw is a date-only string (e.g. '1900-01-01'), new Date(raw) is interpreted as UTC, then setHours(0,0,0,0) shifts it to local midnight. In negative-offset environments (e.g. America/New_York), this becomes 1899-12-31 00:00 local, so the new lower-bound check rejects a value that should be valid per the inclusive 1900-01-01 policy. This regression affects API clients that send birth dates as strings and run on non-UTC servers.

Useful? React with 👍 / 👎.

Comment on lines +113 to +116
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve client date when enforcing 1900-01-01 lower bound

Using getUTC* to derive normalized causes timezone-offset DateTime inputs at midnight to shift to the previous UTC day, so valid boundary values can be rejected. For example, with GraphQL DateTime (see DateTimeScalar.parseValue), sending 1900-01-01T00:00:00+09:00 parses to 1899-12-31T15:00:00Z, then this code normalizes it to 1899-12-31 and throws birthDate is too old, even though the client provided calendar date 1900-01-01. This is a regression introduced by the new minimum-date check path and affects clients that submit full ISO timestamps with positive offsets.

Useful? React with 👍 / 👎.

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;
Expand Down
Loading