From 4e7a443c21d489b79675591f98a54982133ae331 Mon Sep 17 00:00:00 2001 From: chanwoo7 Date: Thu, 30 Apr 2026 01:00:41 +0900 Subject: [PATCH] =?UTF-8?q?feat(user):=20=ED=9A=8C=EC=9B=90=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=88=98=EC=A0=95=EC=97=90=20name=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80=20+=20=ED=95=84=EC=88=98?= =?UTF-8?q?=EA=B0=92=20=EA=B2=80=EC=A6=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 마이페이지 figma 명세에 따라 회원정보 수정 화면에서 이름 변경을 지원한다. '필수값' 표시에 따라 전송 시 trim 후 빈 문자열은 reject. - SDL UpdateMyProfileInput에 name: String 추가 - TS DTO 동기화 - service: hasName 분기 + normalizeName 검증 + repository 호출 - repository: updateProfile args에 name 추가, account/user_profile 양쪽 update를 transaction으로 묶어 부분 실패 방지 - 회귀 테스트 6건 (name 단독/trim/빈값/공백/동시업데이트/미지정 시 유지) --- .../user/repositories/user.repository.ts | 39 ++++++-- .../services/user-profile.service.spec.ts | 92 +++++++++++++++++++ .../user/services/user-profile.service.ts | 14 ++- src/features/user/types/user-input.type.ts | 1 + src/features/user/user-profile.graphql | 2 + 5 files changed, 138 insertions(+), 10 deletions(-) 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 """전화번호"""