-
Notifications
You must be signed in to change notification settings - Fork 0
feat: 카카오 로그인 시 성별·생년월일 기반 유저 정보 자동 생성 #96
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
7fddfc5
0f4c52b
f324a88
5fcce70
7eea265
2e8b72b
8b4aa98
7b69c85
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -41,6 +41,8 @@ | |
| "@prisma/client": "7.2.0", | ||
| "class-transformer": "^0.5.1", | ||
| "class-validator": "^0.14.3", | ||
| "cookie": "^1.1.1", | ||
| "cookie-parser": "^1.4.7", | ||
| "csv-parse": "^6.1.0", | ||
| "jsonwebtoken": "^9.0.3", | ||
| "mariadb": "^3.4.5", | ||
|
|
@@ -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
|
||
| "@types/jest": "^30.0.0", | ||
| "@types/jsonwebtoken": "^9.0.10", | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -5,6 +5,7 @@ import { | |||||||||||||||||||||||
| AddressLevel, | ||||||||||||||||||||||||
| AuthProvider, | ||||||||||||||||||||||||
| Prisma, | ||||||||||||||||||||||||
| Sex, | ||||||||||||||||||||||||
| } from '@prisma/client'; | ||||||||||||||||||||||||
| import { createHash } from 'crypto'; | ||||||||||||||||||||||||
| import type { SignOptions } from 'jsonwebtoken'; | ||||||||||||||||||||||||
|
|
@@ -34,6 +35,9 @@ interface KakaoProfileResponse { | |||||||||||||||||||||||
| profile?: { | ||||||||||||||||||||||||
| nickname?: string; | ||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||
| gender?: string; | ||||||||||||||||||||||||
| birthday?: string; | ||||||||||||||||||||||||
| birthyear?: string; | ||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
@@ -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; | ||||||||||||||||||||||||
|
|
@@ -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 = { | ||||||||||||||||||||||||
|
|
@@ -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, | ||||||||||||||||||||||||
|
|
@@ -160,6 +179,7 @@ export class KakaoAuthService { | |||||||||||||||||||||||
| code: KakaoAuthService.DEFAULT_ADDRESS_CODE, | ||||||||||||||||||||||||
| provider: AuthProvider.KAKAO, | ||||||||||||||||||||||||
| providerUserId, | ||||||||||||||||||||||||
| sex: sex ?? undefined, | ||||||||||||||||||||||||
| vibeVector: {}, | ||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
|
|
@@ -175,6 +195,9 @@ export class KakaoAuthService { | |||||||||||||||||||||||
| data: { | ||||||||||||||||||||||||
| email, | ||||||||||||||||||||||||
| nickname, | ||||||||||||||||||||||||
| ...(birthdate ? { birthdate } : {}), | ||||||||||||||||||||||||
| ...(typeof age === 'number' ? { age } : {}), | ||||||||||||||||||||||||
| ...(sex ? { sex } : {}), | ||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
@@ -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 {}; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
| } | |
| } | |
| // 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 {}; | |
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -19,8 +19,8 @@ export class UserMeResponseDto { | |||||||||||||||||
| @ApiProperty({ example: 'F', enum: Sex }) | ||||||||||||||||||
| gender: Sex; | ||||||||||||||||||
|
|
||||||||||||||||||
|
||||||||||||||||||
| @ApiProperty({ | |
| example: '1970-01-01', | |
| description: '생년월일 (ISO 8601). 향후 age 필드로 대체 예정', | |
| deprecated: true, | |
| }) | |
| birthDate: string; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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
|
||
| code: user.address.code, | ||
| name: areaName, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cookie-parser는src/main.ts에서 사용되지만,cookie패키지는 현재 코드베이스에서 import/사용되는 곳이 확인되지 않습니다. 불필요한 런타임 의존성은 제거하는 게 좋으니cookie를 dependency에서 제외해 주세요(직접 파싱 로직은 이미 삭제됨).