Skip to content
77 changes: 74 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@
"@prisma/client": "7.2.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.3",
"cookie": "^1.1.1",
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

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

cookie-parsersrc/main.ts에서 사용되지만, cookie 패키지는 현재 코드베이스에서 import/사용되는 곳이 확인되지 않습니다. 불필요한 런타임 의존성은 제거하는 게 좋으니 cookie를 dependency에서 제외해 주세요(직접 파싱 로직은 이미 삭제됨).

Suggested change
"cookie": "^1.1.1",

Copilot uses AI. Check for mistakes.
"cookie-parser": "^1.4.7",
"csv-parse": "^6.1.0",
"jsonwebtoken": "^9.0.3",
"mariadb": "^3.4.5",
Expand All @@ -58,6 +60,8 @@
"@nestjs/cli": "^11.0.0",
"@nestjs/schematics": "^11.0.0",
"@nestjs/testing": "^11.0.1",
"@types/cookie": "^0.6.0",
"@types/cookie-parser": "^1.4.10",
"@types/express": "^5.0.0",
Comment on lines 62 to 65
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

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

@types/cookie가 코드에서 사용되지 않는 것으로 보입니다(현재 cookie 모듈도 미사용). 타입 패키지도 함께 제거해서 의존성/락파일 변경 범위를 줄여 주세요.

Copilot uses AI. Check for mistakes.
"@types/jest": "^30.0.0",
"@types/jsonwebtoken": "^9.0.10",
Expand Down
37 changes: 2 additions & 35 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { HttpStatus, ValidationPipe } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { NestFactory } from '@nestjs/core';
import type { ValidationError } from 'class-validator';
import type { Request, NextFunction, Response } from 'express';
import cookieParser from 'cookie-parser';
import pinoHttp from 'pino-http';

import { AppModule } from './modules/app/app.module';
Expand All @@ -25,34 +25,6 @@ function isRequiredError(errors: ValidationError[]): boolean {
});
}

function parseCookieHeader(
cookieHeader: string | undefined,
): Record<string, string> {
if (!cookieHeader) {
return {};
}

const decodeValue = (value: string): string => {
try {
return decodeURIComponent(value);
} catch {
return value;
}
};

return cookieHeader
.split(';')
.reduce<Record<string, string>>((acc, entry) => {
const [rawKey, ...valueParts] = entry.trim().split('=');
if (!rawKey) {
return acc;
}

acc[rawKey] = decodeValue(valueParts.join('='));
return acc;
}, {});
}

async function bootstrap() {
const app = await NestFactory.create(AppModule);

Expand All @@ -79,12 +51,7 @@ async function bootstrap() {
origin: allowedOrigins,
credentials: true,
});
type CookieRequest = Request & { cookies: Record<string, string> };

app.use((req: CookieRequest, _res: Response, next: NextFunction) => {
req.cookies = parseCookieHeader(req.headers.cookie);
next();
});
app.use(cookieParser());

app.useGlobalPipes(
new ValidationPipe({
Expand Down
77 changes: 73 additions & 4 deletions src/modules/auth/services/kakao-auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
AddressLevel,
AuthProvider,
Prisma,
Sex,
} from '@prisma/client';
import { createHash } from 'crypto';
import type { SignOptions } from 'jsonwebtoken';
Expand Down Expand Up @@ -34,6 +35,9 @@ interface KakaoProfileResponse {
profile?: {
nickname?: string;
};
gender?: string;
birthday?: string;
birthyear?: string;
};
}

Expand Down Expand Up @@ -76,11 +80,19 @@ export class KakaoAuthService {
const nickname = nicknameFromProfile ?? `kakao_${providerUserId}`;
const email =
profile.kakao_account?.email ?? `kakao-${providerUserId}@kakao.local`;
const { birthdate, age } = this.buildBirthInfo(
profile.kakao_account?.birthyear,
profile.kakao_account?.birthday,
);
const sex = this.mapSex(profile.kakao_account?.gender);

const userRecord = await this.upsertUser({
providerUserId,
nickname,
email,
birthdate,
age,
sex,
});

const userId = Number(userRecord.user.id) || 0;
Expand Down Expand Up @@ -135,10 +147,16 @@ export class KakaoAuthService {
providerUserId,
nickname,
email,
birthdate,
age,
sex,
}: {
providerUserId: string;
nickname: string;
email: string;
birthdate?: Date;
age?: number;
sex?: Sex;
}) {
await this.ensureDefaultAddress();
const where = {
Expand All @@ -151,7 +169,8 @@ export class KakaoAuthService {
try {
const createdUser = await this.prismaService.user.create({
data: {
birthdate: KakaoAuthService.DEFAULT_BIRTHDATE,
birthdate: birthdate ?? KakaoAuthService.DEFAULT_BIRTHDATE,
age: age ?? undefined,
email,
nickname,
introVoiceUrl: KakaoAuthService.DEFAULT_INTRO_VOICE_URL,
Expand All @@ -160,6 +179,7 @@ export class KakaoAuthService {
code: KakaoAuthService.DEFAULT_ADDRESS_CODE,
provider: AuthProvider.KAKAO,
providerUserId,
sex: sex ?? undefined,
vibeVector: {},
},
});
Expand All @@ -175,6 +195,9 @@ export class KakaoAuthService {
data: {
email,
nickname,
...(birthdate ? { birthdate } : {}),
...(typeof age === 'number' ? { age } : {}),
...(sex ? { sex } : {}),
},
});

Expand Down Expand Up @@ -254,16 +277,62 @@ export class KakaoAuthService {
}
}

private buildBirthInfo(
birthyear?: string,
birthday?: string,
): { birthdate?: Date; age?: number } {
if (!birthyear || !birthday || birthday.length !== 4) {
return {};
}

const year = Number(birthyear);
const month = Number(birthday.slice(0, 2));
const day = Number(birthday.slice(2));
if (!Number.isInteger(year) || !Number.isInteger(month)) {
return {};
}
if (month < 1 || month > 12 || day < 1 || day > 31) {
return {};
}

const birthdate = new Date(Date.UTC(year, month - 1, day));
if (Number.isNaN(birthdate.getTime())) {
return {};
}
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

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

buildBirthInfo에서 new Date(Date.UTC(year, month - 1, day))는 잘못된 날짜(예: 02/31)를 자동으로 다음 달로 보정해 유효한 Date를 만들어버리므로, 현재의 NaN 체크로는 invalid birthday를 걸러내지 못합니다. 생성된 birthdate의 UTC year/month/day가 입력값과 정확히 일치하는지 추가 검증(또는 엄격한 파싱)을 넣어 잘못된 생일이 저장/나이 계산에 반영되지 않게 해주세요.

Suggested change
}
}
// Ensure that the constructed UTC date matches the input components exactly.
// This prevents dates like "0231" (Feb 31) from rolling over into the next month.
if (
birthdate.getUTCFullYear() !== year ||
birthdate.getUTCMonth() + 1 !== month ||
birthdate.getUTCDate() !== day
) {
return {};
}

Copilot uses AI. Check for mistakes.

const today = new Date();
const currentYear = today.getUTCFullYear();
const currentMonth = today.getUTCMonth() + 1;
const currentDay = today.getUTCDate();
let age = currentYear - year;
if (currentMonth < month || (currentMonth === month && currentDay < day)) {
age -= 1;
}
if (age < 0) {
return {};
}

return { birthdate, age };
}

private mapSex(gender?: string): Sex | undefined {
const normalized = gender?.toLowerCase();
if (normalized === 'male') {
return Sex.M;
}
if (normalized === 'female') {
return Sex.F;
}
return undefined;
}

private isOnboardingRequired(user: {
birthdate: Date;
introText: string;
introVoiceUrl: string;
profileImageUrl: string;
code: string;
}) {
return (
user.birthdate.getTime() ===
KakaoAuthService.DEFAULT_BIRTHDATE.getTime() ||
user.introText.trim() === '' ||
user.introVoiceUrl === KakaoAuthService.DEFAULT_INTRO_VOICE_URL ||
user.profileImageUrl === KakaoAuthService.DEFAULT_PROFILE_IMAGE_URL ||
Expand Down
4 changes: 2 additions & 2 deletions src/modules/user/dtos/user-me-response.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ export class UserMeResponseDto {
@ApiProperty({ example: 'F', enum: Sex })
gender: Sex;

Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

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

UserMeResponseDto에서 birthDate를 제거하고 age로 대체하면서 /users/me 응답 스키마가 깨지는 변경이 들어갔습니다. 현재 다른 DTO/로직에서는 여전히 birthDate를 사용하고 있어(예: 프로필 업데이트/온보딩) API 일관성이 깨집니다. 호환성을 위해 birthDate를 유지하고 age를 추가로 제공하거나, 버전업/마이그레이션 계획을 포함해 변경을 조정해 주세요.

Suggested change
@ApiProperty({
example: '1970-01-01',
description: '생년월일 (ISO 8601). 향후 age 필드로 대체 예정',
deprecated: true,
})
birthDate: string;

Copilot uses AI. Check for mistakes.
@ApiProperty({ example: '1972-03-01' })
birthDate: string;
@ApiProperty({ example: 53, description: '만 나이' })
age: number;

@ApiProperty({ type: UserAreaDto })
area: UserAreaDto;
Expand Down
2 changes: 1 addition & 1 deletion src/modules/user/repositories/user.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export class UserRepository {
id: true,
nickname: true,
sex: true,
birthdate: true,
age: true,
introText: true,
introVoiceUrl: true,
profileImageUrl: true,
Expand Down
5 changes: 2 additions & 3 deletions src/modules/user/services/user/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,12 @@ export class UserService {
const idealPersonalities = user.idealPersonalities
.map((item) => item.personality.body)
.filter((body): body is string => Boolean(body));
const birthDate = user.birthdate.toISOString().split('T')[0];

const age = user.age;
return {
userId: Number(user.id),
nickname: user.nickname,
gender: user.sex,
birthDate,
age,
area: {
Comment on lines +35 to 41
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

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

getMe 응답을 age로 바꾸면, 사용자가 birthDate를 수정하는 기존 플로우(예: updateMe에서 birthdate 업데이트)가 있어도 age가 함께 갱신되지 않아 데이터가 쉽게 불일치할 수 있습니다. birthdate 변경 시 age를 재계산/동기화하거나, age를 저장값이 아닌 계산값으로 제공하는 방식으로 일관성을 보장해 주세요.

Copilot uses AI. Check for mistakes.
code: user.address.code,
name: areaName,
Expand Down
Loading