From ed5ec3ab054bd56747ebde67de746f8f7a487dd0 Mon Sep 17 00:00:00 2001 From: hyewon Date: Tue, 11 Nov 2025 12:58:59 +0900 Subject: [PATCH 1/4] =?UTF-8?q?[NUGUDI-181]=20feat(web):=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=95=84=20=EC=88=98=EC=A0=95=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 사용자 엔티티와 어댑터에 키·몸무게 속성 추가 - 프로필 수정 폼 스키마와 기본 정보/신체 정보 컴포넌트 구현 - 프로필 수정 페이지에서 신규 뷰 컴포넌트 렌더링 --- apps/web/app/(auth)/profile/edit/page.tsx | 4 +- .../user/domain/entities/user.entity.ts | 18 +++ .../schemas/profile-edit.schema.ts | 19 +++ .../shared/adapters/user.adapter.ts | 13 +- .../presentation/shared/types/user.type.ts | 2 + .../user-basic-info-form-component/index.tsx | 108 ++++++++++++++ .../index.tsx | 141 ++++++++++++++++++ .../index.tsx | 69 +++++++++ .../ui/views/user-profile-edit-view/index.tsx | 12 ++ 9 files changed, 375 insertions(+), 11 deletions(-) create mode 100644 apps/web/src/domains/user/presentation/schemas/profile-edit.schema.ts create mode 100644 apps/web/src/domains/user/presentation/ui/components/user-basic-info-form-component/index.tsx create mode 100644 apps/web/src/domains/user/presentation/ui/components/user-physical-info-form-component/index.tsx create mode 100644 apps/web/src/domains/user/presentation/ui/sections/user-profile-edit-form-section-mock/index.tsx create mode 100644 apps/web/src/domains/user/presentation/ui/views/user-profile-edit-view/index.tsx diff --git a/apps/web/app/(auth)/profile/edit/page.tsx b/apps/web/app/(auth)/profile/edit/page.tsx index 1814bf03..f8040614 100644 --- a/apps/web/app/(auth)/profile/edit/page.tsx +++ b/apps/web/app/(auth)/profile/edit/page.tsx @@ -1,5 +1,7 @@ +import { UserProfileEditView } from "@/src/domains/user/presentation/ui/views/user-profile-edit-view"; + const ProfileEditPage = () => { - return
ProfileEditPage
; + return ; }; export default ProfileEditPage; diff --git a/apps/web/src/domains/user/domain/entities/user.entity.ts b/apps/web/src/domains/user/domain/entities/user.entity.ts index 393ef485..e29deb76 100644 --- a/apps/web/src/domains/user/domain/entities/user.entity.ts +++ b/apps/web/src/domains/user/domain/entities/user.entity.ts @@ -23,6 +23,8 @@ export interface UserProfile { getNickname(): string; getEmail(): string | undefined; getProfileImageUrl(): string | undefined; + getHeight(): number | undefined; + getWeight(): number | undefined; getCreatedAt(): string | undefined; getUpdatedAt(): string | undefined; @@ -59,6 +61,8 @@ export class UserProfileEntity implements UserProfile { private readonly _nickname: string; private readonly _email?: string; private readonly _profileImageUrl?: string; + private readonly _height?: number; + private readonly _weight?: number; private readonly _createdAt?: string; private readonly _updatedAt?: string; @@ -67,6 +71,8 @@ export class UserProfileEntity implements UserProfile { nickname: string; email?: string; profileImageUrl?: string; + height?: number; + weight?: number; createdAt?: string; updatedAt?: string; }) { @@ -123,6 +129,8 @@ export class UserProfileEntity implements UserProfile { this._nickname = params.nickname; this._email = params.email; this._profileImageUrl = params.profileImageUrl; + this._height = params.height; + this._weight = params.weight; this._createdAt = params.createdAt; this._updatedAt = params.updatedAt; } @@ -144,6 +152,14 @@ export class UserProfileEntity implements UserProfile { return this._profileImageUrl; } + getHeight(): number | undefined { + return this._height; + } + + getWeight(): number | undefined { + return this._weight; + } + getCreatedAt(): string | undefined { return this._createdAt; } @@ -279,6 +295,8 @@ export class UserProfileEntity implements UserProfile { nickname: this._nickname, email: this._email, profileImageUrl: this._profileImageUrl, + height: this._height, + weight: this._weight, createdAt: this._createdAt, updatedAt: this._updatedAt, }; diff --git a/apps/web/src/domains/user/presentation/schemas/profile-edit.schema.ts b/apps/web/src/domains/user/presentation/schemas/profile-edit.schema.ts new file mode 100644 index 00000000..c943fc38 --- /dev/null +++ b/apps/web/src/domains/user/presentation/schemas/profile-edit.schema.ts @@ -0,0 +1,19 @@ +import { z } from "zod"; + +export const profileEditSchema = z.object({ + nickname: z + .string() + .min(2, "닉네임은 최소 2자 이상이어야 합니다") + .max(10, "닉네임은 최대 10자까지 가능합니다") + .regex( + /^[a-zA-Z0-9가-힣ㄱ-ㅎㅏ-ㅣ]+$/, + "닉네임은 영문, 한글, 숫자만 사용할 수 있습니다", + ) + .refine((val) => !val.includes(" "), { + message: "닉네임에 공백을 포함할 수 없습니다", + }), + height: z.number().optional(), + weight: z.number().optional(), +}); + +export type ProfileEditFormData = z.infer; diff --git a/apps/web/src/domains/user/presentation/shared/adapters/user.adapter.ts b/apps/web/src/domains/user/presentation/shared/adapters/user.adapter.ts index d3903ce5..e6bfda65 100644 --- a/apps/web/src/domains/user/presentation/shared/adapters/user.adapter.ts +++ b/apps/web/src/domains/user/presentation/shared/adapters/user.adapter.ts @@ -1,15 +1,6 @@ import type { UserProfile } from "../../../domain/entities/user.entity"; import type { UserProfileItem } from "../types/user.type"; -type UserProfileData = { - id: number; - nickname: string; - email?: string; - profileImageUrl?: string; - createdAt?: string; - updatedAt?: string; -}; - export const UserAdapter = { /** * Transform UserProfile entity to UI item @@ -25,6 +16,8 @@ export const UserAdapter = { nickname: profile.getNickname(), email: profile.getEmail(), profileImageUrl: profile.getProfileImageUrl(), + height: profile.getHeight(), + weight: profile.getWeight(), createdAt: profile.getCreatedAt(), updatedAt: profile.getUpdatedAt(), }; @@ -170,7 +163,7 @@ export const UserAdapter = { * @param changes - Partial profile data with proposed changes * @returns True if profile can be updated with these changes */ - canEdit(profile: UserProfile, changes: Partial): boolean { + canEdit(profile: UserProfile, changes: Partial): boolean { if ( "canUpdateProfile" in profile && typeof profile.canUpdateProfile === "function" diff --git a/apps/web/src/domains/user/presentation/shared/types/user.type.ts b/apps/web/src/domains/user/presentation/shared/types/user.type.ts index 74188c34..b3416f30 100644 --- a/apps/web/src/domains/user/presentation/shared/types/user.type.ts +++ b/apps/web/src/domains/user/presentation/shared/types/user.type.ts @@ -3,6 +3,8 @@ export interface UserProfileItem { nickname: string; email?: string; profileImageUrl?: string; + height?: number; + weight?: number; createdAt?: string; updatedAt?: string; } diff --git a/apps/web/src/domains/user/presentation/ui/components/user-basic-info-form-component/index.tsx b/apps/web/src/domains/user/presentation/ui/components/user-basic-info-form-component/index.tsx new file mode 100644 index 00000000..86b2a6de --- /dev/null +++ b/apps/web/src/domains/user/presentation/ui/components/user-basic-info-form-component/index.tsx @@ -0,0 +1,108 @@ +import { Button } from "@nugudi/react-components-button"; +import { Input } from "@nugudi/react-components-input"; +import { Box, Flex, Title, VStack } from "@nugudi/react-components-layout"; +import type { FieldErrors, UseFormRegister } from "react-hook-form"; +import type { ProfileEditFormData } from "../../../schemas/profile-edit.schema"; + +interface UserBasicInfoFormComponentProps { + register: UseFormRegister; + errors: FieldErrors; + defaultNickname: string; + onCheckNickname: () => void; + onNicknameChange: (e: React.ChangeEvent) => void; + isCheckingNickname: boolean; +} + +export const UserBasicInfoFormComponent = ({ + register, + errors, + defaultNickname, + onCheckNickname, + onNicknameChange, + isCheckingNickname, +}: UserBasicInfoFormComponentProps) => { + return ( + + + + + ); +}; + +function UserBasicInfoTitle() { + return 기본 정보; +} + +type UserNicknameFieldProps = Pick< + UserBasicInfoFormComponentProps, + | "register" + | "errors" + | "defaultNickname" + | "onCheckNickname" + | "onNicknameChange" + | "isCheckingNickname" +>; + +function UserNicknameField({ + register, + errors, + defaultNickname, + onCheckNickname, + onNicknameChange, + isCheckingNickname, +}: UserNicknameFieldProps) { + return ( + + + { + register("nickname").onChange(e); + onNicknameChange(e); + }} + placeholder="정조이" + isError={!!errors.nickname} + errorMessage={errors.nickname?.message} + /> + + + + ); +} + +interface NicknameDuplicateCheckButtonProps + extends Pick< + UserBasicInfoFormComponentProps, + "onCheckNickname" | "isCheckingNickname" + > {} + +function NicknameDuplicateCheckButton({ + onCheckNickname, + isCheckingNickname, +}: NicknameDuplicateCheckButtonProps) { + return ( + + ); +} diff --git a/apps/web/src/domains/user/presentation/ui/components/user-physical-info-form-component/index.tsx b/apps/web/src/domains/user/presentation/ui/components/user-physical-info-form-component/index.tsx new file mode 100644 index 00000000..c232e066 --- /dev/null +++ b/apps/web/src/domains/user/presentation/ui/components/user-physical-info-form-component/index.tsx @@ -0,0 +1,141 @@ +import { Input } from "@nugudi/react-components-input"; +import { Body, HStack, Title, VStack } from "@nugudi/react-components-layout"; +import type { FieldErrors, UseFormRegister } from "react-hook-form"; +import type { ProfileEditFormData } from "../../../schemas/profile-edit.schema"; + +interface UserPhysicalInfoFormComponentProps { + register: UseFormRegister; + errors: FieldErrors; + defaultHeight?: number; + defaultWeight?: number; +} + +export const UserPhysicalInfoFormComponent = ({ + register, + errors, + defaultHeight, + defaultWeight, +}: UserPhysicalInfoFormComponentProps) => { + return ( + + + + + ); +}; + +function UserPhysicalInfoHeader() { + return ( + + 신체 정보 + + + ); +} + +function UserPhysicalInfoDescription() { + return ( + + 체형별 맞춤 서비스를 위해 필요하며 다른 사람에게 공개되지 않습니다 + + ); +} + +interface UserPhysicalInfoInputFieldsProps + extends Pick< + UserPhysicalInfoFormComponentProps, + "register" | "errors" | "defaultHeight" | "defaultWeight" + > {} + +function UserPhysicalInfoInputFields({ + register, + errors, + defaultHeight, + defaultWeight, +}: UserPhysicalInfoInputFieldsProps) { + return ( + + + + + ); +} + +interface UserHeightInputProps + extends Pick< + UserPhysicalInfoFormComponentProps, + "register" | "errors" | "defaultHeight" + > {} + +function UserHeightInput({ + register, + errors, + defaultHeight, +}: UserHeightInputProps) { + return ( + (v === "" ? undefined : Number(v)), + })} + defaultValue={defaultHeight} + type="number" + placeholder="163" + isError={!!errors.height} + rightIcon={} + /> + ); +} + +interface UserWeightInputProps + extends Pick< + UserPhysicalInfoFormComponentProps, + "register" | "errors" | "defaultWeight" + > {} + +function UserWeightInput({ + register, + errors, + defaultWeight, +}: UserWeightInputProps) { + return ( + (v === "" ? undefined : Number(v)), + })} + defaultValue={defaultWeight} + type="number" + placeholder="80" + isError={!!errors.weight} + rightIcon={} + /> + ); +} + +interface PhysicalUnitTextProps { + unit: string; +} + +function PhysicalUnitText({ unit }: PhysicalUnitTextProps) { + return ( + + {unit} + + ); +} diff --git a/apps/web/src/domains/user/presentation/ui/sections/user-profile-edit-form-section-mock/index.tsx b/apps/web/src/domains/user/presentation/ui/sections/user-profile-edit-form-section-mock/index.tsx new file mode 100644 index 00000000..16d26185 --- /dev/null +++ b/apps/web/src/domains/user/presentation/ui/sections/user-profile-edit-form-section-mock/index.tsx @@ -0,0 +1,69 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { Button } from "@nugudi/react-components-button"; +import { VStack } from "@nugudi/react-components-layout"; +import { useRouter } from "next/navigation"; +import { useForm } from "react-hook-form"; +import { + type ProfileEditFormData, + profileEditSchema, +} from "../../../schemas/profile-edit.schema"; +import { UserBasicInfoFormComponent } from "../../components/user-basic-info-form-component"; +import { UserPhysicalInfoFormComponent } from "../../components/user-physical-info-form-component"; + +const MOCK_PROFILE = { + nickname: "정조이", + height: 163, + weight: 40, +}; + +export const UserProfileEditFormSectionMock = () => { + const router = useRouter(); + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ + resolver: zodResolver(profileEditSchema), + defaultValues: MOCK_PROFILE, + }); + + const onSubmit = (data: ProfileEditFormData) => { + console.log("폼 제출:", data); + router.push("/profile"); + }; + + return ( + + + console.log("닉네임 중복 체크")} + onNicknameChange={() => {}} + isCheckingNickname={false} + /> + + + + + + + ); +}; diff --git a/apps/web/src/domains/user/presentation/ui/views/user-profile-edit-view/index.tsx b/apps/web/src/domains/user/presentation/ui/views/user-profile-edit-view/index.tsx new file mode 100644 index 00000000..514a4f8e --- /dev/null +++ b/apps/web/src/domains/user/presentation/ui/views/user-profile-edit-view/index.tsx @@ -0,0 +1,12 @@ +import { VStack } from "@nugudi/react-components-layout"; +import { NavBar } from "@/src/core/ui"; +import { UserProfileEditFormSectionMock } from "../../sections/user-profile-edit-form-section-mock"; + +export const UserProfileEditView = () => { + return ( + + + + + ); +}; From bebc2968b1431089230757a121118daba5a283ae Mon Sep 17 00:00:00 2001 From: hyewon Date: Tue, 11 Nov 2025 13:24:40 +0900 Subject: [PATCH 2/4] =?UTF-8?q?[NUGUDI-181]=20feat(web):=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=95=84=20=EC=84=B9=EC=85=98=20UI=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 인턴 너구리 이미지를 추가하고 기본 프로필 이미지를 교체 - 배지와 타이포그래피 컴포넌트로 프로필 정보 표현을 재구성 - 프로필 뷰에 스택 레이아웃을 적용해 섹션 배치를 정리했습니다 - 수정 버튼 범위 클릭 범위 확장 --- apps/web/public/images/intern-nuguri.png | Bin 0 -> 18464 bytes .../user-profile-section/index.css.ts | 23 ++++-- .../sections/user-profile-section/index.tsx | 74 +++++++++++------- .../ui/views/user-profile-view/index.tsx | 8 +- 4 files changed, 67 insertions(+), 38 deletions(-) create mode 100644 apps/web/public/images/intern-nuguri.png diff --git a/apps/web/public/images/intern-nuguri.png b/apps/web/public/images/intern-nuguri.png new file mode 100644 index 0000000000000000000000000000000000000000..a1e2a01528e6441b342394dc375b310cda3d292c GIT binary patch literal 18464 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 FV1kF5w>|&> literal 0 HcmV?d00001 diff --git a/apps/web/src/domains/user/presentation/shared/ui/sections/user-profile-section/index.css.ts b/apps/web/src/domains/user/presentation/shared/ui/sections/user-profile-section/index.css.ts index 32401873..8f173220 100644 --- a/apps/web/src/domains/user/presentation/shared/ui/sections/user-profile-section/index.css.ts +++ b/apps/web/src/domains/user/presentation/shared/ui/sections/user-profile-section/index.css.ts @@ -4,13 +4,14 @@ import { style } from "@vanilla-extract/css"; export const container = style({ width: "100%", gap: "16px", - padding: "16px", + paddingLeft: vars.box.spacing[4], + paddingRight: vars.box.spacing[4], boxSizing: "border-box", alignItems: "center", }); export const profileImage = style({ - objectFit: "cover", + objectFit: "contain", }); export const infoWrapper = style({ @@ -26,18 +27,24 @@ export const levelText = style({ color: vars.colors.$scale.main[500], }); -export const nameText = style({ - ...classes.typography.title.t3, - fontWeight: vars.typography.fontWeight[600], - color: vars.colors.$scale.zinc[600], -}); - export const nameSuffix = style({ ...classes.typography.body.b4, }); export const editButton = style({ + position: "relative", + display: "inline-flex", + alignItems: "center", + justifyContent: "center", color: vars.colors.$scale.zinc[600], + cursor: "pointer", + transition: "color 0.2s ease", + "::before": { + content: '""', + position: "absolute", + inset: "-12px", + zIndex: 0, + }, ":hover": { color: vars.colors.$scale.main[500], }, diff --git a/apps/web/src/domains/user/presentation/shared/ui/sections/user-profile-section/index.tsx b/apps/web/src/domains/user/presentation/shared/ui/sections/user-profile-section/index.tsx index c2794b75..5df0e99b 100644 --- a/apps/web/src/domains/user/presentation/shared/ui/sections/user-profile-section/index.tsx +++ b/apps/web/src/domains/user/presentation/shared/ui/sections/user-profile-section/index.tsx @@ -1,7 +1,8 @@ "use client"; import { PencilIcon } from "@nugudi/assets-icons"; -import { Flex } from "@nugudi/react-components-layout"; +import { Badge } from "@nugudi/react-components-badge"; +import { Body, Flex, HStack, Title } from "@nugudi/react-components-layout"; import Image from "next/image"; import Link from "next/link"; import { Suspense } from "react"; @@ -24,12 +25,17 @@ export const UserProfileSection = () => { const UserProfileSectionSkeleton = () => { return ( -
+
-
-
+
+ +
+ +
+
+ + -
); }; @@ -40,23 +46,30 @@ const UserProfileSectionError = () => { profile - Lv.1 기본 너구리 - -

- 손님 -

+ + Lv.1 + + + + 인턴 너구리 + + + + 손님 + + + + +
- - -
); }; @@ -72,23 +85,30 @@ const UserProfileSectionContent = () => { profile - Lv.1 기본 너구리 - -

- {nickname} -

+ + Lv.1 + + + + 인턴 너구리 + + + + {nickname} + + + + +
- - -
); }; diff --git a/apps/web/src/domains/user/presentation/shared/ui/views/user-profile-view/index.tsx b/apps/web/src/domains/user/presentation/shared/ui/views/user-profile-view/index.tsx index f73149d6..0f7f95e1 100644 --- a/apps/web/src/domains/user/presentation/shared/ui/views/user-profile-view/index.tsx +++ b/apps/web/src/domains/user/presentation/shared/ui/views/user-profile-view/index.tsx @@ -1,5 +1,5 @@ import { AppHeader } from "@core/ui/components/app-header"; -import { Flex } from "@nugudi/react-components-layout"; +import { Flex, VStack } from "@nugudi/react-components-layout"; import { UserProfileLogoutButton } from "../../components/user-profile-logout-button"; import { UserProfileMenuSection } from "../../sections/user-profile-menu-section"; import { UserProfilePointSection } from "../../sections/user-profile-point-section"; @@ -10,8 +10,10 @@ export const UserProfileView = () => { return ( - - + + + + From ceae61465fdfa17b121741d11093db2e5439859e Mon Sep 17 00:00:00 2001 From: hyewon Date: Tue, 11 Nov 2025 13:32:05 +0900 Subject: [PATCH 3/4] =?UTF-8?q?[NUGUDI-181]=20fix(web):=20=ED=8F=AC?= =?UTF-8?q?=EC=9D=B8=ED=8A=B8=20=ED=99=94=EB=A9=B4=20=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=95=84=EC=9B=83=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - navbar 변경으로 인한 깨진 UI 수정 --- .../shared/ui/components/user-points-balance/index.css.ts | 1 + .../shared/ui/components/user-points-balance/index.tsx | 2 +- .../ui/sections/user-points-history-section/index.css.ts | 6 ++++++ .../ui/sections/user-points-history-section/index.tsx | 3 ++- .../shared/ui/views/user-points-view/index.css.ts | 1 + .../presentation/shared/ui/views/user-points-view/index.tsx | 6 ++---- 6 files changed, 13 insertions(+), 6 deletions(-) create mode 100644 apps/web/src/domains/user/presentation/shared/ui/sections/user-points-history-section/index.css.ts diff --git a/apps/web/src/domains/user/presentation/shared/ui/components/user-points-balance/index.css.ts b/apps/web/src/domains/user/presentation/shared/ui/components/user-points-balance/index.css.ts index 353c145c..b9e34b26 100644 --- a/apps/web/src/domains/user/presentation/shared/ui/components/user-points-balance/index.css.ts +++ b/apps/web/src/domains/user/presentation/shared/ui/components/user-points-balance/index.css.ts @@ -3,6 +3,7 @@ import { style } from "@vanilla-extract/css"; export const container = style({ position: "relative", height: 190, + boxSizing: "border-box", }); export const imageWrapper = style({ diff --git a/apps/web/src/domains/user/presentation/shared/ui/components/user-points-balance/index.tsx b/apps/web/src/domains/user/presentation/shared/ui/components/user-points-balance/index.tsx index 39623dbd..e0c4410e 100644 --- a/apps/web/src/domains/user/presentation/shared/ui/components/user-points-balance/index.tsx +++ b/apps/web/src/domains/user/presentation/shared/ui/components/user-points-balance/index.tsx @@ -17,7 +17,7 @@ interface UserPointsBalanceProps { export const UserPointsBalance = ({ balance }: UserPointsBalanceProps) => { return ( - + diff --git a/apps/web/src/domains/user/presentation/shared/ui/sections/user-points-history-section/index.css.ts b/apps/web/src/domains/user/presentation/shared/ui/sections/user-points-history-section/index.css.ts new file mode 100644 index 00000000..09290ccb --- /dev/null +++ b/apps/web/src/domains/user/presentation/shared/ui/sections/user-points-history-section/index.css.ts @@ -0,0 +1,6 @@ +import { vars } from "@nugudi/themes"; +import { style } from "@vanilla-extract/css"; + +export const container = style({ + backgroundColor: vars.colors.$static.light.color.white, +}); diff --git a/apps/web/src/domains/user/presentation/shared/ui/sections/user-points-history-section/index.tsx b/apps/web/src/domains/user/presentation/shared/ui/sections/user-points-history-section/index.tsx index 326dac8e..6f662616 100644 --- a/apps/web/src/domains/user/presentation/shared/ui/sections/user-points-history-section/index.tsx +++ b/apps/web/src/domains/user/presentation/shared/ui/sections/user-points-history-section/index.tsx @@ -5,13 +5,14 @@ import { type DailyPointData, UserPointsHistoryList, } from "../../components/user-points-history-list"; +import * as styles from "./index.css"; export const UserPointsHistorySection = () => { // TODO: Replace with actual data from API const pointsData = usePointsData(); return ( - + ); diff --git a/apps/web/src/domains/user/presentation/shared/ui/views/user-points-view/index.css.ts b/apps/web/src/domains/user/presentation/shared/ui/views/user-points-view/index.css.ts index b7556fd2..30a99bf8 100644 --- a/apps/web/src/domains/user/presentation/shared/ui/views/user-points-view/index.css.ts +++ b/apps/web/src/domains/user/presentation/shared/ui/views/user-points-view/index.css.ts @@ -3,6 +3,7 @@ import { style } from "@vanilla-extract/css"; export const container = style({ boxSizing: "border-box", + backgroundColor: vars.colors.$static.light.zinc[50], }); export const content = style({ diff --git a/apps/web/src/domains/user/presentation/shared/ui/views/user-points-view/index.tsx b/apps/web/src/domains/user/presentation/shared/ui/views/user-points-view/index.tsx index e9f39f67..d134f372 100644 --- a/apps/web/src/domains/user/presentation/shared/ui/views/user-points-view/index.tsx +++ b/apps/web/src/domains/user/presentation/shared/ui/views/user-points-view/index.tsx @@ -15,10 +15,8 @@ export const UserPointsView = () => { w="full" className={styles.container} > - - - - + + ); From 1aee6e277263b4dec364419ff9cc599a1304da33 Mon Sep 17 00:00:00 2001 From: hyewon Date: Tue, 11 Nov 2025 14:16:31 +0900 Subject: [PATCH 4/4] =?UTF-8?q?[NUGUDI-181]=20fix(web):=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=95=84=20=EC=88=98=EC=A0=95=20=ED=8F=BC=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=20=EA=B2=80=EC=A6=9D=20=EB=B0=8F=20=ED=83=80=EC=9E=85?= =?UTF-8?q?=20=EC=95=88=EC=A0=95=EC=84=B1=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 키/몸무게 필드에 범위 검증 추가 (키: 130-220cm, 몸무게: 30-250kg) - Number.isNaN() 사용하여 타입 안정성 강화 - 빈 값 및 잘못된 입력 처리를 위한 setValueAs 핸들러 개선 - react-hook-form defaultValues와 중복되는 defaultValue props 제거 --- .../schemas/profile-edit.schema.ts | 12 ++++++++++-- .../user-basic-info-form-component/index.tsx | 6 ------ .../index.tsx | 19 +++++++++++-------- .../index.tsx | 8 +------- 4 files changed, 22 insertions(+), 23 deletions(-) diff --git a/apps/web/src/domains/user/presentation/schemas/profile-edit.schema.ts b/apps/web/src/domains/user/presentation/schemas/profile-edit.schema.ts index c943fc38..3b309850 100644 --- a/apps/web/src/domains/user/presentation/schemas/profile-edit.schema.ts +++ b/apps/web/src/domains/user/presentation/schemas/profile-edit.schema.ts @@ -12,8 +12,16 @@ export const profileEditSchema = z.object({ .refine((val) => !val.includes(" "), { message: "닉네임에 공백을 포함할 수 없습니다", }), - height: z.number().optional(), - weight: z.number().optional(), + height: z + .number() + .min(130, "키는 최소 130cm 이상이어야 합니다") + .max(220, "키는 최대 220cm까지 입력 가능합니다") + .optional(), + weight: z + .number() + .min(30, "몸무게는 최소 30kg 이상이어야 합니다") + .max(250, "몸무게는 최대 250kg까지 입력 가능합니다") + .optional(), }); export type ProfileEditFormData = z.infer; diff --git a/apps/web/src/domains/user/presentation/ui/components/user-basic-info-form-component/index.tsx b/apps/web/src/domains/user/presentation/ui/components/user-basic-info-form-component/index.tsx index 86b2a6de..bcf04778 100644 --- a/apps/web/src/domains/user/presentation/ui/components/user-basic-info-form-component/index.tsx +++ b/apps/web/src/domains/user/presentation/ui/components/user-basic-info-form-component/index.tsx @@ -7,7 +7,6 @@ import type { ProfileEditFormData } from "../../../schemas/profile-edit.schema"; interface UserBasicInfoFormComponentProps { register: UseFormRegister; errors: FieldErrors; - defaultNickname: string; onCheckNickname: () => void; onNicknameChange: (e: React.ChangeEvent) => void; isCheckingNickname: boolean; @@ -16,7 +15,6 @@ interface UserBasicInfoFormComponentProps { export const UserBasicInfoFormComponent = ({ register, errors, - defaultNickname, onCheckNickname, onNicknameChange, isCheckingNickname, @@ -27,7 +25,6 @@ export const UserBasicInfoFormComponent = ({ { register("nickname").onChange(e); onNicknameChange(e); diff --git a/apps/web/src/domains/user/presentation/ui/components/user-physical-info-form-component/index.tsx b/apps/web/src/domains/user/presentation/ui/components/user-physical-info-form-component/index.tsx index c232e066..9f12f1bb 100644 --- a/apps/web/src/domains/user/presentation/ui/components/user-physical-info-form-component/index.tsx +++ b/apps/web/src/domains/user/presentation/ui/components/user-physical-info-form-component/index.tsx @@ -90,7 +90,11 @@ function UserHeightInput({ label="키" variant="filled" {...register("height", { - setValueAs: (v) => (v === "" ? undefined : Number(v)), + setValueAs: (v) => { + if (!v || v === "") return undefined; + const n = Number(v); + return Number.isNaN(n) ? undefined : n; + }, })} defaultValue={defaultHeight} type="number" @@ -107,19 +111,18 @@ interface UserWeightInputProps "register" | "errors" | "defaultWeight" > {} -function UserWeightInput({ - register, - errors, - defaultWeight, -}: UserWeightInputProps) { +function UserWeightInput({ register, errors }: UserWeightInputProps) { return ( (v === "" ? undefined : Number(v)), + setValueAs: (v) => { + if (!v || v === "") return undefined; + const n = Number(v); + return Number.isNaN(n) ? undefined : n; + }, })} - defaultValue={defaultWeight} type="number" placeholder="80" isError={!!errors.weight} diff --git a/apps/web/src/domains/user/presentation/ui/sections/user-profile-edit-form-section-mock/index.tsx b/apps/web/src/domains/user/presentation/ui/sections/user-profile-edit-form-section-mock/index.tsx index 16d26185..965d6100 100644 --- a/apps/web/src/domains/user/presentation/ui/sections/user-profile-edit-form-section-mock/index.tsx +++ b/apps/web/src/domains/user/presentation/ui/sections/user-profile-edit-form-section-mock/index.tsx @@ -47,18 +47,12 @@ export const UserProfileEditFormSectionMock = () => { console.log("닉네임 중복 체크")} onNicknameChange={() => {}} isCheckingNickname={false} /> - +