diff --git a/src/features/user/repositories/user.repository.ts b/src/features/user/repositories/user.repository.ts index 5d4739d..d917fed 100644 --- a/src/features/user/repositories/user.repository.ts +++ b/src/features/user/repositories/user.repository.ts @@ -96,18 +96,39 @@ export class UserRepository { async updateProfile(args: { accountId: bigint; nickname?: string; + name?: string; birthDate?: Date | null; phoneNumber?: string | null; }): Promise { - await this.prisma.userProfile.update({ - where: { account_id: args.accountId }, - data: { - ...(args.nickname !== undefined ? { nickname: args.nickname } : {}), - ...(args.birthDate !== undefined ? { birth_date: args.birthDate } : {}), - ...(args.phoneNumber !== undefined - ? { phone_number: args.phoneNumber } - : {}), - }, + const hasName = args.name !== undefined; + const hasProfileFields = + args.nickname !== undefined || + args.birthDate !== undefined || + args.phoneNumber !== undefined; + + // name은 account 테이블, 나머지는 user_profile 테이블이라 + // 두 테이블 부분 실패 방지를 위해 transaction으로 묶는다. + await this.prisma.$transaction(async (tx) => { + if (hasName) { + await tx.account.update({ + where: { id: args.accountId }, + data: { name: args.name }, + }); + } + if (hasProfileFields) { + await tx.userProfile.update({ + where: { account_id: args.accountId }, + data: { + ...(args.nickname !== undefined ? { nickname: args.nickname } : {}), + ...(args.birthDate !== undefined + ? { birth_date: args.birthDate } + : {}), + ...(args.phoneNumber !== undefined + ? { phone_number: args.phoneNumber } + : {}), + }, + }); + } }); } diff --git a/src/features/user/services/user-profile.service.spec.ts b/src/features/user/services/user-profile.service.spec.ts index e5af413..f0711c8 100644 --- a/src/features/user/services/user-profile.service.spec.ts +++ b/src/features/user/services/user-profile.service.spec.ts @@ -222,6 +222,98 @@ describe('UserProfileService (real DB)', () => { service.updateMyProfile(me.id, { nickname: 'taken' }), ).rejects.toThrow(ConflictException); }); + + it('name만 단독 업데이트하면 Account.name이 갱신된다', async () => { + const account = await createAccount(prisma, { + account_type: 'USER', + name: '구이름', + }); + await createUserProfile(prisma, { account_id: account.id }); + + const result = await service.updateMyProfile(account.id, { + name: '새이름', + }); + + expect(result.name).toBe('새이름'); + const saved = await prisma.account.findUniqueOrThrow({ + where: { id: account.id }, + }); + expect(saved.name).toBe('새이름'); + }); + + it('name 입력 시 trim 후 저장한다', async () => { + const account = await createAccount(prisma, { + account_type: 'USER', + name: '구이름', + }); + await createUserProfile(prisma, { account_id: account.id }); + + const result = await service.updateMyProfile(account.id, { + name: ' 홍길동 ', + }); + + expect(result.name).toBe('홍길동'); + }); + + it('name이 빈 문자열이면 BadRequestException', async () => { + const account = await createAccount(prisma, { + account_type: 'USER', + name: '구이름', + }); + await createUserProfile(prisma, { account_id: account.id }); + + await expect( + service.updateMyProfile(account.id, { name: '' }), + ).rejects.toThrow(BadRequestException); + }); + + it('name이 공백-only이면 BadRequestException', async () => { + const account = await createAccount(prisma, { + account_type: 'USER', + name: '구이름', + }); + await createUserProfile(prisma, { account_id: account.id }); + + await expect( + service.updateMyProfile(account.id, { name: ' ' }), + ).rejects.toThrow(BadRequestException); + }); + + it('name + nickname 동시 업데이트 시 둘 다 반영된다', async () => { + const account = await createAccount(prisma, { + account_type: 'USER', + name: '구이름', + }); + await createUserProfile(prisma, { + account_id: account.id, + nickname: 'oldNick', + }); + + const result = await service.updateMyProfile(account.id, { + name: '새이름', + nickname: 'newNick', + }); + + expect(result.name).toBe('새이름'); + expect(result.profile.nickname).toBe('newNick'); + }); + + it('name 미지정 시 기존 Account.name이 유지된다', async () => { + const account = await createAccount(prisma, { + account_type: 'USER', + name: '유지될이름', + }); + await createUserProfile(prisma, { account_id: account.id }); + + await service.updateMyProfile(account.id, { + nickname: 'newNick', + }); + + const saved = await prisma.account.findUniqueOrThrow({ + where: { id: account.id }, + }); + expect(saved.name).toBe('유지될이름'); + }); }); // ─── updateMyProfileImage ─── diff --git a/src/features/user/services/user-profile.service.ts b/src/features/user/services/user-profile.service.ts index 6c32d01..9564388 100644 --- a/src/features/user/services/user-profile.service.ts +++ b/src/features/user/services/user-profile.service.ts @@ -73,10 +73,11 @@ export class UserProfileService extends UserBaseService { await this.requireActiveUser(accountId); const hasNickname = input.nickname !== undefined; + const hasName = input.name !== undefined; const hasBirthDate = input.birthDate !== undefined; const hasPhoneNumber = input.phoneNumber !== undefined; - if (!hasNickname && !hasBirthDate && !hasPhoneNumber) { + if (!hasNickname && !hasName && !hasBirthDate && !hasPhoneNumber) { throw new BadRequestException('No fields to update.'); } @@ -89,6 +90,16 @@ export class UserProfileService extends UserBaseService { if (isTaken) throw new ConflictException('Nickname already exists.'); } + // figma 명세: 이름은 필수값. 전송되었지만 trim 후 빈 문자열이면 reject. + let name: string | undefined = undefined; + if (hasName) { + const normalized = this.normalizeName(input.name); + if (!normalized) { + throw new BadRequestException('Name cannot be empty.'); + } + name = normalized; + } + const birthDate = hasBirthDate ? this.normalizeBirthDate(input.birthDate) : undefined; @@ -99,6 +110,7 @@ export class UserProfileService extends UserBaseService { await this.repo.updateProfile({ accountId, ...(hasNickname ? { nickname } : {}), + ...(hasName ? { name } : {}), ...(hasBirthDate ? { birthDate } : {}), ...(hasPhoneNumber ? { phoneNumber } : {}), }); diff --git a/src/features/user/types/user-input.type.ts b/src/features/user/types/user-input.type.ts index 3fc9ce9..d0edb35 100644 --- a/src/features/user/types/user-input.type.ts +++ b/src/features/user/types/user-input.type.ts @@ -7,6 +7,7 @@ export interface CompleteOnboardingInput { export interface UpdateMyProfileInput { nickname?: string | null; + name?: string | null; birthDate?: Date | null; phoneNumber?: string | null; } diff --git a/src/features/user/user-profile.graphql b/src/features/user/user-profile.graphql index f4865ac..a52b9f5 100644 --- a/src/features/user/user-profile.graphql +++ b/src/features/user/user-profile.graphql @@ -62,6 +62,8 @@ input CompleteOnboardingInput { input UpdateMyProfileInput { """닉네임""" nickname: String + """이름(전송 시 trim 후 비어있으면 reject)""" + name: String """생년월일""" birthDate: DateTime """전화번호"""