Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion apps/web/app/(auth)/profile/edit/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { UserProfileEditView } from "@/src/domains/user/presentation/ui/views/user-profile-edit-view";

const ProfileEditPage = () => {
return <div>ProfileEditPage</div>;
return <UserProfileEditView />;
};

export default ProfileEditPage;
Binary file added apps/web/public/images/intern-nuguri.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions apps/web/src/domains/user/domain/entities/user.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;

Expand All @@ -67,6 +71,8 @@ export class UserProfileEntity implements UserProfile {
nickname: string;
email?: string;
profileImageUrl?: string;
height?: number;
weight?: number;
createdAt?: string;
updatedAt?: string;
}) {
Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
}
Expand Down Expand Up @@ -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,
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
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()
.min(130, "키는 최소 130cm 이상이어야 합니다")
.max(220, "키는 최대 220cm까지 입력 가능합니다")
.optional(),
weight: z
.number()
.min(30, "몸무게는 최소 30kg 이상이어야 합니다")
.max(250, "몸무게는 최대 250kg까지 입력 가능합니다")
.optional(),
});

export type ProfileEditFormData = z.infer<typeof profileEditSchema>;
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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(),
};
Expand Down Expand Up @@ -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<UserProfileData>): boolean {
canEdit(profile: UserProfile, changes: Partial<UserProfileItem>): boolean {
if (
"canUpdateProfile" in profile &&
typeof profile.canUpdateProfile === "function"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ export interface UserProfileItem {
nickname: string;
email?: string;
profileImageUrl?: string;
height?: number;
weight?: number;
createdAt?: string;
updatedAt?: string;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ interface UserPointsBalanceProps {

export const UserPointsBalance = ({ balance }: UserPointsBalanceProps) => {
return (
<VStack gap={8} width="full" className={styles.container}>
<VStack gap={8} width="full" className={styles.container} pX={16}>
<MyPointInfo balance={balance} />
<NuguriImage />
</VStack>
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
});
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<VStack gap={16}>
<VStack gap={16} pY={16} grow={1} className={styles.container}>
<UserPointsHistoryList pointsData={pointsData} />
</VStack>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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],
},
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -24,12 +25,17 @@ export const UserProfileSection = () => {
const UserProfileSectionSkeleton = () => {
return (
<Flex className={styles.container}>
<div className="h-[60px] w-[60px] animate-pulse rounded-full bg-zinc-200" />
<div className="h-[130px] w-[165px] animate-pulse bg-zinc-200" />
<Flex className={styles.infoWrapper}>
<div className="h-5 w-32 animate-pulse rounded bg-zinc-200" />
<div className="h-7 w-24 animate-pulse rounded bg-zinc-200" />
<div className="h-5 w-10 animate-pulse rounded bg-zinc-200" />
<Flex direction="column" gap={4}>
<div className="h-6 w-24 animate-pulse rounded bg-zinc-200" />
<Flex gap={4} align="center">
<div className="h-5 w-16 animate-pulse rounded bg-zinc-200" />
<div className="h-4 w-4 animate-pulse rounded bg-zinc-200" />
</Flex>
</Flex>
</Flex>
<div className="h-10 w-10 animate-pulse rounded-full bg-zinc-200" />
</Flex>
);
};
Expand All @@ -40,23 +46,30 @@ const UserProfileSectionError = () => {
<Flex className={styles.container}>
<Image
priority
src="/images/nuguri.webp"
src="/images/intern-nuguri.png"
alt="profile"
width={60}
height={60}
width={165}
height={130}
className={styles.profileImage}
/>
<Flex className={styles.infoWrapper}>
<span className={styles.levelText}>Lv.1 기본 너구리</span>
<Flex gap={4} align="end">
<h1 className={styles.nameText}>
손님 <span className={styles.nameSuffix}>님</span>
</h1>
<Badge tone="positive" size="xs" variant="weak">
Lv.1
</Badge>
<Flex direction="column" gap={4}>
<Title fontSize="t3" color="main" colorShade={800}>
인턴 너구리
</Title>
<HStack gap={4} align="center">
<Body fontSize="b3b" colorShade={700}>
손님
</Body>
<Link href="/profile/edit" className={styles.editButton}>
<PencilIcon width={16} height={16} />
</Link>
</HStack>
</Flex>
</Flex>
<Link href="/profile/edit" className={styles.editButton}>
<PencilIcon />
</Link>
</Flex>
);
};
Expand All @@ -72,23 +85,30 @@ const UserProfileSectionContent = () => {
<Flex className={styles.container}>
<Image
priority
src={profileImageUrl ?? "/images/nuguri.webp"}
src={profileImageUrl ?? "/images/intern-nuguri.png"}
alt="profile"
width={60}
height={60}
width={165}
height={130}
className={styles.profileImage}
/>
<Flex className={styles.infoWrapper}>
<span className={styles.levelText}>Lv.1 기본 너구리</span>
<Flex gap={4} align="end">
<h1 className={styles.nameText}>
{nickname} <span className={styles.nameSuffix}>님</span>
</h1>
<Badge tone="positive" size="xs" variant="weak">
Lv.1
</Badge>
<Flex direction="column" gap={4}>
<Title fontSize="t3" color="main" colorShade={800}>
인턴 너구리
</Title>
<HStack gap={4} align="center">
<Body fontSize="b3b" colorShade={700}>
{nickname}
</Body>
<Link href="/profile/edit" className={styles.editButton}>
<PencilIcon width={16} height={16} />
</Link>
</HStack>
</Flex>
Comment on lines +95 to 110
Copy link

@coderabbitai coderabbitai bot Nov 11, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

에러 상태와 콘텐츠 상태의 UI가 중복됩니다.

Line 57-72의 에러 UI와 Line 96-111의 실제 콘텐츠 UI가 거의 동일한 구조입니다. 공통 컴포넌트로 추출하여 코드 중복을 제거할 수 있습니다.

다음과 같이 공통 컴포넌트로 추출할 수 있습니다:

interface UserProfileDisplayProps {
  profileImageUrl: string;
  nickname: string;
  level?: string;
  title?: string;
}

const UserProfileDisplay = ({
  profileImageUrl,
  nickname,
  level = "Lv.1",
  title = "인턴 너구리"
}: UserProfileDisplayProps) => {
  return (
    <Flex className={styles.container}>
      <Image
        priority
        src={profileImageUrl}
        alt="profile"
        width={165}
        height={130}
        className={styles.profileImage}
      />
      <Flex className={styles.infoWrapper}>
        <Badge tone="positive" size="xs" variant="weak">
          {level}
        </Badge>
        <Flex direction="column" gap={4}>
          <Title fontSize="t3" color="main" colorShade={800}>
            {title}
          </Title>
          <HStack gap={4} align="center">
            <Body fontSize="b3b" colorShade={700}>
              {nickname}
            </Body>
            <Link href="/profile/edit" className={styles.editButton}>
              <PencilIcon width={16} height={16} />
            </Link>
          </HStack>
        </Flex>
      </Flex>
    </Flex>
  );
};

// 그리고 각 컴포넌트에서:
const UserProfileSectionError = () => (
  <UserProfileDisplay
    profileImageUrl="/images/intern-nuguri.png"
    nickname="손님"
  />
);

const UserProfileSectionContent = () => {
  const { data: profileData } = useSuspenseQuery(userProfileQueryClient);
  return (
    <UserProfileDisplay
      profileImageUrl={profileData.profileImageUrl ?? "/images/intern-nuguri.png"}
      nickname={profileData.nickname ?? "손님"}
    />
  );
};
🤖 Prompt for AI Agents
In
apps/web/src/domains/user/presentation/ui/sections/user-profile-section/index.tsx
around lines 57-72 and 96-111, the error-state UI and the real-content UI are
duplicated; extract a shared presentational component (e.g., UserProfileDisplay)
that accepts profileImageUrl, nickname, level, and title props and use it for
both the error and content variants; update the error variant to pass default
placeholder values and the content variant to pass values from profileData (with
fallbacks), and replace the duplicated JSX blocks with calls to the new shared
component while keeping existing className/style/props intact.

Copy link
Member Author

Choose a reason for hiding this comment

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

현재 브랜치는 프로필 수정 컴포넌트와 관련되어있으므로, 이 부분은 추후 실제로 프로필 연결 과정에서 수정하도록 하겟습니다.

Copy link

Choose a reason for hiding this comment

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

@hijjoy, 이해했습니다! 현재 PR의 범위를 고려하면 합리적인 결정입니다.

추후 프로필 연결 작업 시 이 리팩토링을 잊지 않도록 후속 이슈를 생성하시겠습니까? 원하신다면 제가 이슈를 만들어 드릴 수 있습니다.

</Flex>
<Link href="/profile/edit" className={styles.editButton}>
<PencilIcon />
</Link>
</Flex>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,8 @@ export const UserPointsView = () => {
w="full"
className={styles.container}
>
<VStack gap={24} p={16} className={styles.content}>
<NavBar title="내 포인트" />
<UserPointsBalanceSection />
</VStack>
<NavBar title="내 포인트" background="transparent" />
<UserPointsBalanceSection />
<UserPointsHistorySection />
</VStack>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -10,8 +10,10 @@ export const UserProfileView = () => {
return (
<Flex direction="column" className={styles.container}>
<AppHeader />
<UserProfileSection />
<UserProfilePointSection />
<VStack w="full">
<UserProfileSection />
<UserProfilePointSection />
</VStack>
<UserProfileMenuSection />
<UserProfileLogoutButton />
</Flex>
Expand Down
Loading