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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,5 @@ next-env.d.ts
# local env files
.env.local
/.vscode
/docs
/docs
/.github/copilot-instructions.md
50 changes: 50 additions & 0 deletions src/app/api/user/nickname/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { cookies } from "next/headers";
import { NextResponse } from "next/server";

import { createClient as createServerClient } from "@/shared/utils/supabase/server";

export async function PATCH(request: Request) {
try {
const supabase = createServerClient(cookies());
const { data: userData, error: userError } = await supabase.auth.getUser();

if (userError || !userData.user) {
return NextResponse.json({ status: "error", data: null, error: "Unauthorized" }, { status: 401 });
}

const body = await request.json();
const { nickname } = body;

// 닉네임 검증
if (!nickname || typeof nickname !== "string") {
return NextResponse.json({ status: "error", data: null, error: "닉네임을 입력해주세요." }, { status: 400 });
}

if (nickname.length < 2 || nickname.length > 20) {
return NextResponse.json(
{ status: "error", data: null, error: "닉네임은 2자 이상 20자 이하로 입력해주세요." },
{ status: 400 },
);
}
Comment on lines +16 to +28
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

닉네임에 대한 유효성 검사를 강화하는 것을 제안합니다. 현재 코드는 앞뒤 공백이 포함된 닉네임을 허용할 수 있습니다. trim()을 사용하여 입력값의 양 끝에 있는 공백을 제거한 후 유효성을 검사하는 것이 좋습니다. 이렇게 하면 " test "와 같은 닉네임이 데이터베이스에 저장되는 것을 방지할 수 있습니다.

Suggested change
const { nickname } = body;
// 닉네임 검증
if (!nickname || typeof nickname !== "string") {
return NextResponse.json({ status: "error", data: null, error: "닉네임을 입력해주세요." }, { status: 400 });
}
if (nickname.length < 2 || nickname.length > 20) {
return NextResponse.json(
{ status: "error", data: null, error: "닉네임은 2자 이상 20자 이하로 입력해주세요." },
{ status: 400 },
);
}
const { nickname: rawNickname } = body;
const nickname = typeof rawNickname === "string" ? rawNickname.trim() : rawNickname;
// 닉네임 검증
if (!nickname || typeof nickname !== "string") {
return NextResponse.json({ status: "error", data: null, error: "닉네임을 입력해주세요." }, { status: 400 });
}
if (nickname.length < 2 || nickname.length > 20) {
return NextResponse.json(
{ status: "error", data: null, error: "닉네임은 2자 이상 20자 이하로 입력해주세요." },
{ status: 400 },
);
}

Comment on lines +18 to +28
Copy link

Copilot AI Nov 22, 2025

Choose a reason for hiding this comment

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

Consider adding input sanitization for the nickname. While length validation is present, consider trimming whitespace before validation and storage to prevent nicknames with only spaces or leading/trailing whitespace. Apply .trim() to the nickname before the validation checks on line 19.

Copilot uses AI. Check for mistakes.

// 사용자 메타데이터 업데이트
const { data, error } = await supabase.auth.updateUser({
data: {
nickname,
},
});
Comment on lines +30 to +35
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

PR 설명에 언급된 대로 닉네임 중복 검사 로직을 추가하는 것이 중요합니다. 중복 닉네임을 허용하면 사용자 혼란을 야기하고 다른 사용자를 사칭하는 데 사용될 수 있습니다. Supabase DB에서 사용자를 조회하여 user_metadata의 닉네임이 고유한지 확인하는 로직을 updateUser 호출 전에 추가하는 것을 권장합니다. 이는 데이터 무결성과 보안을 위해 높은 우선순위로 처리해야 할 사항입니다.


if (error) {
console.error("Nickname update error:", error);
return NextResponse.json({ status: "error", data: null, error: error.message }, { status: 500 });
}

return NextResponse.json(
{ status: "success", data: { nickname: data.user.user_metadata.nickname } },
Copy link

Copilot AI Nov 22, 2025

Choose a reason for hiding this comment

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

Potential null reference error. The code accesses data.user.user_metadata.nickname on line 43 without checking if data or data.user could be null. While the error check on line 37 should prevent this, add defensive checks or use optional chaining (data?.user?.user_metadata?.nickname) to prevent potential runtime errors.

Suggested change
{ status: "success", data: { nickname: data.user.user_metadata.nickname } },
{ status: "success", data: { nickname: data?.user?.user_metadata?.nickname } },

Copilot uses AI. Check for mistakes.
{ status: 200 },
);
} catch (error) {
console.error("Nickname API error:", error);
return NextResponse.json({ status: "error", data: null, error: "Internal server error" }, { status: 500 });
}
}
15 changes: 13 additions & 2 deletions src/app/setting/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
export default function SettingsPage() {
return <div>Settings Page</div>;
import { ProfileSection, UserInfoSection } from "@/features";

export default function SettingPage() {
return (
<div className='flex w-full max-w-4xl flex-col gap-8 px-5 py-10'>
<h1 className='text-2xl font-bold text-white'>프로필 설정</h1>

<div className='flex flex-col gap-8 md:flex-row md:items-start'>
<ProfileSection />
<UserInfoSection />
</div>
</div>
);
}
1 change: 1 addition & 0 deletions src/features/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from "./home";
export * from "./login";
export * from "./setting";
export * from "./signup";
export * from "./wallet";
26 changes: 26 additions & 0 deletions src/features/setting/apis/change-nickname.api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { APIResponse } from "@/shared";

export interface ChangeNicknameParams {
nickname: string;
}

export type ChangeNicknameResponse = APIResponse<{
nickname: string;
}>;

export const changeNicknameAPI = async ({ nickname }: ChangeNicknameParams): Promise<ChangeNicknameResponse> => {
const response = await fetch("/api/user/nickname", {
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ nickname }),
});

if (!response.ok) {
const error = await response.json();
throw new Error(error.error || "닉네임 변경에 실패했습니다.");
Comment on lines +21 to +22
Copy link

Copilot AI Nov 22, 2025

Choose a reason for hiding this comment

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

Missing error handling for JSON parsing. If the error response is not valid JSON, await response.json() on line 21 will throw an exception. Consider wrapping this in a try-catch block or handling cases where the response body is not JSON (e.g., network errors, server errors without JSON body).

Suggested change
const error = await response.json();
throw new Error(error.error || "닉네임 변경에 실패했습니다.");
let errorMessage = "닉네임 변경에 실패했습니다.";
try {
const error = await response.json();
if (error && error.error) {
errorMessage = error.error;
}
} catch (e) {
// response body is not valid JSON, keep default errorMessage
}
throw new Error(errorMessage);

Copilot uses AI. Check for mistakes.
}

return response.json();
};
1 change: 1 addition & 0 deletions src/features/setting/apis/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./change-nickname.api";
Empty file.
18 changes: 18 additions & 0 deletions src/features/setting/components/features/EmailField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"use client";

import { Label, Skeleton } from "@/shared";

import { useUserInfo } from "../../hooks";

export const EmailField = () => {
const { userEmail, isLoaded } = useUserInfo();

return (
<div className='flex items-center gap-2'>
<Label className='text-md text-text-primary w-16'>이메일</Label>
<div className='flex h-10 min-w-70 items-center rounded-md bg-background px-3 py-2'>
{!isLoaded ? <Skeleton className='h-5 w-32' /> : <p className='text-text-muted text-sm'>{userEmail}</p>}
</div>
</div>
);
};
25 changes: 25 additions & 0 deletions src/features/setting/components/features/LastLoginField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"use client";

import { Label, Skeleton } from "@/shared";

import { useUserInfo } from "../../hooks";
import { formattedDate } from "../../utils";

export const LastLoginField = () => {
const { lastUpdated, isLoaded } = useUserInfo();

const formattedLastUpdated = formattedDate(lastUpdated || "");

return (
<div className='flex items-center gap-4'>
Copy link

Copilot AI Nov 22, 2025

Choose a reason for hiding this comment

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

[nitpick] Inconsistent gap values across form fields. The email and nickname fields use gap-2 while the last login field uses gap-4. Consider standardizing the gap value (e.g., gap-2 for all) for visual consistency.

Suggested change
<div className='flex items-center gap-4'>
<div className='flex items-center gap-2'>

Copilot uses AI. Check for mistakes.
<Label className='text-text-primary w-20'>마지막 접속일</Label>
Copy link

Copilot AI Nov 22, 2025

Choose a reason for hiding this comment

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

[nitpick] Inconsistent label widths across form fields. The email and nickname fields use w-16 while the last login field uses w-20. Consider using consistent widths across all labels (e.g., w-20 for all) to maintain visual alignment.

Suggested change
<Label className='text-text-primary w-20'>마지막 접속일</Label>
<Label className='text-text-primary w-16'>마지막 접속일</Label>

Copilot uses AI. Check for mistakes.
<div className='bg-surface-light/30 flex h-10 min-w-70 items-center px-3 py-2'>
{!isLoaded ? (
<Skeleton className='h-5 w-32' />
) : (
<p className='text-text-muted text-sm'>{formattedLastUpdated}</p>
)}
</div>
</div>
);
};
37 changes: 37 additions & 0 deletions src/features/setting/components/features/NicknameField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"use client";

import { Button, Input, Label, Skeleton, Spinner } from "@/shared";

import { useChangeNickname, useUserInfo } from "../../hooks";

export const NicknameField = () => {
const { userNickname, isLoaded } = useUserInfo();

const { fieldState, inputValue, onChangeNickname, onClickEditButton, isPending } = useChangeNickname({
userNickname,
});

return (
<div className='flex items-center gap-2'>
<Label className='text-md text-text-primary w-16'>닉네임</Label>
<div className='flex items-center gap-4'>
{fieldState === "edit" ? (
<Input
className='min-w-70'
value={inputValue}
onChange={onChangeNickname}
disabled={isPending}
placeholder='닉네임을 입력하세요'
/>
Comment on lines +16 to +25
Copy link

Copilot AI Nov 22, 2025

Choose a reason for hiding this comment

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

Missing proper label association for the nickname input field. The Label component on line 16 is not associated with the Input component on line 19 using the htmlFor attribute. Add an id prop to the Input and a corresponding htmlFor prop to the Label to properly associate them for accessibility (e.g., <Label htmlFor="nickname" ...> and <Input id="nickname" ...>).

Suggested change
<Label className='text-md text-text-primary w-16'>닉네임</Label>
<div className='flex items-center gap-4'>
{fieldState === "edit" ? (
<Input
className='min-w-70'
value={inputValue}
onChange={onChangeNickname}
disabled={isPending}
placeholder='닉네임을 입력하세요'
/>
<Label htmlFor="nickname" className='text-md text-text-primary w-16'>닉네임</Label>
<div className='flex items-center gap-4'>
{fieldState === "edit" ? (
<Input
id="nickname"
className='min-w-70'
value={inputValue}
onChange={onChangeNickname}
disabled={isPending}
placeholder='닉네임을 입력하세요'

Copilot uses AI. Check for mistakes.
) : (
<div className='flex h-10 min-w-70 items-center rounded-md bg-background px-3 py-2'>
{!isLoaded ? <Skeleton className='h-5 w-32' /> : <p className='text-text-muted text-sm'>{userNickname}</p>}
Copy link

Copilot AI Nov 22, 2025

Choose a reason for hiding this comment

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

[nitpick] Hardcoded skeleton width may not match actual content width. The skeleton uses w-32 but the actual email/nickname length can vary significantly. Consider making the skeleton width match the expected content width or use a more flexible width (e.g., w-full or a percentage-based width) for better visual fidelity during loading.

Suggested change
{!isLoaded ? <Skeleton className='h-5 w-32' /> : <p className='text-text-muted text-sm'>{userNickname}</p>}
{!isLoaded ? <Skeleton className='h-5 w-full' /> : <p className='text-text-muted text-sm'>{userNickname}</p>}

Copilot uses AI. Check for mistakes.
</div>
)}
<Button onClick={onClickEditButton} variant='outline' className='h-10' disabled={isPending}>
{isPending ? <Spinner /> : fieldState === "edit" ? "저장" : "수정"}
</Button>
</div>
</div>
);
};
3 changes: 3 additions & 0 deletions src/features/setting/components/features/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from "./EmailField";
export * from "./LastLoginField";
export * from "./NicknameField";
1 change: 1 addition & 0 deletions src/features/setting/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./features";
2 changes: 2 additions & 0 deletions src/features/setting/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./useUserInfo";
export * from "./useChangeNickname";
62 changes: 62 additions & 0 deletions src/features/setting/hooks/useChangeNickname.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { useEffect, useState } from "react";

import { useMutation } from "@tanstack/react-query";
import { toast } from "sonner";

import { createClient } from "@/shared/utils/supabase";

import { changeNicknameAPI } from "../apis";

type Props = {
userNickname: string;
};

export const useChangeNickname = ({ userNickname }: Props) => {
const [fieldState, setFieldState] = useState<"view" | "edit">("view");
const [inputValue, setInputValue] = useState(userNickname);
const supabase = createClient();

// userNickname이 변경되면 inputValue 동기화 (세션 새로고침 후)
useEffect(() => {
setInputValue(userNickname);
}, [userNickname]);

const onChangeNickname = (e: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(e.target.value);
};

const { mutate: changeNickname, isPending } = useMutation({
mutationFn: () => changeNicknameAPI({ nickname: inputValue }),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

API로 닉네임을 보내기 전에 inputValue의 양쪽 공백을 제거하는 것이 좋습니다. 이렇게 하면 불필요한 공백이 데이터베이스에 저장되는 것을 방지하고, onClickEditButtoninputValue.trim() === userNickname 비교 로직과 일관성을 유지할 수 있습니다.

Suggested change
mutationFn: () => changeNicknameAPI({ nickname: inputValue }),
mutationFn: () => changeNicknameAPI({ nickname: inputValue.trim() }),

onError: (error: Error) => {
toast.error(error.message);
setInputValue(userNickname); // 에러 시 원래 값으로 복구
},
onSuccess: async () => {
toast.success("닉네임이 변경되었습니다.");
setFieldState("view");

// Supabase 세션 강제 갱신 (AuthProvider가 감지하여 Zustand 스토어 업데이트)
await supabase.auth.refreshSession();
},
});

const onClickEditButton = () => {
if (fieldState === "edit") {
if (inputValue.trim() === userNickname) {
Copy link

Copilot AI Nov 22, 2025

Choose a reason for hiding this comment

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

Inconsistent trimming logic. The comparison trims inputValue but compares it to untrimmed userNickname. If the original nickname has leading/trailing spaces, this comparison could behave unexpectedly. Consider trimming both values or ensuring the comparison matches the actual validation logic used in the API.

Suggested change
if (inputValue.trim() === userNickname) {
if (inputValue.trim() === userNickname.trim()) {

Copilot uses AI. Check for mistakes.
setFieldState("view");
return;
}
changeNickname();
} else {
setFieldState("edit");
}
};
Comment on lines +43 to +53
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

사용자 경험을 개선하기 위해 닉네임 변경 API를 호출하기 전에 클라이언트 측에서 유효성 검사를 추가하는 것이 좋습니다. 현재는 서버에서만 유효성 검사를 수행하므로, 유효하지 않은 닉네임(예: 1글자)을 입력하고 저장 버튼을 누르면 API 호출 후에야 에러 메시지가 표시됩니다. onClickEditButton 함수에 닉네임 길이를 확인하는 로직을 추가하면 사용자에게 즉각적인 피드백을 줄 수 있습니다.

  const onClickEditButton = () => {
    if (fieldState === "edit") {
      const trimmedNickname = inputValue.trim();
      if (trimmedNickname === userNickname) {
        setFieldState("view");
        return;
      }

      if (trimmedNickname.length < 2 || trimmedNickname.length > 20) {
        toast.error("닉네임은 2자 이상 20자 이하로 입력해주세요.");
        return;
      }

      changeNickname();
    } else {
      setFieldState("edit");
    }
  };

Comment on lines +43 to +53
Copy link

Copilot AI Nov 22, 2025

Choose a reason for hiding this comment

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

[nitpick] Missing client-side validation before API call. The nickname validation (length check, trim, etc.) is only done on the server side. Consider adding client-side validation in the onClickEditButton function to provide immediate feedback to users before making the API request. For example, check if inputValue.trim().length < 2 or > 20 and show an error toast.

Copilot uses AI. Check for mistakes.

return {
fieldState,
inputValue,
onChangeNickname,
onClickEditButton,
Comment on lines +54 to +59
Copy link

Copilot AI Nov 22, 2025

Choose a reason for hiding this comment

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

[nitpick] Missing cancel functionality when in edit mode. When a user clicks the edit button and enters edit mode but wants to discard changes, there's no way to cancel and revert to the original value without saving. Consider adding a separate "취소" (Cancel) button or allowing the edit button to cancel when the value hasn't changed (the current check at line 45 only works if the value matches the original).

Suggested change
return {
fieldState,
inputValue,
onChangeNickname,
onClickEditButton,
const onCancelEdit = () => {
setInputValue(userNickname);
setFieldState("view");
};
return {
fieldState,
inputValue,
onChangeNickname,
onClickEditButton,
onCancelEdit,

Copilot uses AI. Check for mistakes.
isPending,
};
};
15 changes: 15 additions & 0 deletions src/features/setting/hooks/useUserInfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"use client";

import { useGetSession, useIsSessionLoaded } from "@/shared";

export const useUserInfo = () => {
const session = useGetSession();
const isLoaded = useIsSessionLoaded();

const userAvatarUrl = session?.user?.user_metadata?.avatar_url || "";
const userNickname = session?.user?.user_metadata?.nickname || "";
const userEmail = session?.user?.email || "";
const lastUpdated = session?.user?.updated_at || "";

return { userAvatarUrl, userNickname, userEmail, lastUpdated, isLoaded };
};
1 change: 1 addition & 0 deletions src/features/setting/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./ui";
31 changes: 31 additions & 0 deletions src/features/setting/ui/ProfileSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"use client";

import { Avatar, AvatarFallback, AvatarImage, Button } from "@/shared";
import { Camera, UserRound } from "lucide-react";

import { useUserInfo } from "../hooks";

export const ProfileSection = () => {
const { userAvatarUrl } = useUserInfo();

return (
<section className='flex flex-col items-center gap-4 bg-surface-dark p-10'>
<div className='relative'>
<Avatar className='border-surface-light size-32 border-2'>
<AvatarImage src={userAvatarUrl} alt='User Avatar' className='object-cover' />
<AvatarFallback className='bg-surface-light'>
<UserRound className='text-text-muted size-16' />
</AvatarFallback>
</Avatar>
<Button
size='icon'
variant='secondary'
className='hover:bg-surface-light absolute right-0 bottom-0 size-10 rounded-full shadow-lg'
Copy link

Copilot AI Nov 22, 2025

Choose a reason for hiding this comment

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

Missing accessibility attributes for profile picture upload button. The camera button should include an aria-label to describe its purpose for screen readers (e.g., aria-label="프로필 사진 업로드" or aria-label="Upload profile picture").

Suggested change
className='hover:bg-surface-light absolute right-0 bottom-0 size-10 rounded-full shadow-lg'
className='hover:bg-surface-light absolute right-0 bottom-0 size-10 rounded-full shadow-lg'
aria-label="프로필 사진 업로드"

Copilot uses AI. Check for mistakes.
>
<Camera className='size-5' />
</Button>
</div>
<p className='text-text-muted text-sm'>프로필 사진 변경</p>
</section>
);
};
11 changes: 11 additions & 0 deletions src/features/setting/ui/UserInfoSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { EmailField, LastLoginField, NicknameField } from "../components";

export const UserInfoSection = () => {
return (
<section className='flex w-full flex-col items-start justify-center gap-6 bg-surface-dark p-9'>
<EmailField />
<NicknameField />
<LastLoginField />
</section>
);
};
2 changes: 2 additions & 0 deletions src/features/setting/ui/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./ProfileSection";
export * from "./UserInfoSection";
13 changes: 13 additions & 0 deletions src/features/setting/utils/formatted-date.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export const formattedDate = (dateString: string): string => {
const date = dateString ? new Date(dateString) : null;

const year = date ? date.getFullYear() : "";
const month = date ? String(date.getMonth() + 1).padStart(2, "0") : "";
const day = date ? String(date.getDate()).padStart(2, "0") : "";

const Hour = date ? String(date.getHours()).padStart(2, "0") : "";
const Minutes = date ? String(date.getMinutes()).padStart(2, "0") : "";
const Seconds = date ? String(date.getSeconds()).padStart(2, "0") : "";

Comment on lines +4 to +11
Copy link

Copilot AI Nov 22, 2025

Choose a reason for hiding this comment

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

[nitpick] Potential issue with formatted date output when dateString is empty. When an empty string is passed, the function returns "년 월 일 ::" which may not be user-friendly. Consider returning a more meaningful message like "정보 없음" or "N/A" when the date is invalid or empty.

Suggested change
const year = date ? date.getFullYear() : "";
const month = date ? String(date.getMonth() + 1).padStart(2, "0") : "";
const day = date ? String(date.getDate()).padStart(2, "0") : "";
const Hour = date ? String(date.getHours()).padStart(2, "0") : "";
const Minutes = date ? String(date.getMinutes()).padStart(2, "0") : "";
const Seconds = date ? String(date.getSeconds()).padStart(2, "0") : "";
// Check for empty, null, or invalid date
if (!dateString || !date || isNaN(date.getTime())) {
return "정보 없음";
}
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
const Hour = String(date.getHours()).padStart(2, "0");
const Minutes = String(date.getMinutes()).padStart(2, "0");
const Seconds = String(date.getSeconds()).padStart(2, "0");

Copilot uses AI. Check for mistakes.
return `${year}년 ${month}월 ${day}일 ${Hour}:${Minutes}:${Seconds}`;
};
Comment on lines +1 to +13
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

formattedDate 함수에 몇 가지 개선 사항을 제안합니다.

  1. 버그 수정: dateString이 비어있을 때 "년 월 일 : :" 와 같은 잘못된 형식의 문자열이 반환됩니다. 유효하지 않은 입력에 대해서는 "-" 같은 대체 텍스트를 반환하도록 수정해야 합니다.
  2. 유효성 검사 강화: new Date(dateString)Invalid Date를 반환하는 경우에 대한 처리를 추가하여 안정성을 높일 수 있습니다.
  3. 네이밍 컨벤션: 변수명 Hour, Minutes, Seconds를 JavaScript/TypeScript 표준인 camelCase (hour, minutes, seconds)로 변경하여 코드 스타일의 일관성을 맞추는 것이 좋습니다.
Suggested change
export const formattedDate = (dateString: string): string => {
const date = dateString ? new Date(dateString) : null;
const year = date ? date.getFullYear() : "";
const month = date ? String(date.getMonth() + 1).padStart(2, "0") : "";
const day = date ? String(date.getDate()).padStart(2, "0") : "";
const Hour = date ? String(date.getHours()).padStart(2, "0") : "";
const Minutes = date ? String(date.getMinutes()).padStart(2, "0") : "";
const Seconds = date ? String(date.getSeconds()).padStart(2, "0") : "";
return `${year}${month}${day}${Hour}:${Minutes}:${Seconds}`;
};
export const formattedDate = (dateString: string): string => {
if (!dateString) {
return "-";
}
const date = new Date(dateString);
if (isNaN(date.getTime())) {
return "-";
}
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
const hour = String(date.getHours()).padStart(2, "0");
const minutes = String(date.getMinutes()).padStart(2, "0");
const seconds = String(date.getSeconds()).padStart(2, "0");
return `${year}${month}${day}${hour}:${minutes}:${seconds}`;
};

1 change: 1 addition & 0 deletions src/features/setting/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./formatted-date";
7 changes: 2 additions & 5 deletions src/shared/components/features/header/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import Image from "next/image";
import Link from "next/link";

import { UserRound, Wallet } from "lucide-react";

import Logo from "@/shared/assets/logo.webp";

import { ROUTER_PATH } from "../../../constants";
import { LoginButton } from "../../common";
import { Avatar, AvatarFallback, AvatarImage } from "../../ui";

export const Header = () => {
return (
Expand Down Expand Up @@ -35,10 +32,10 @@ export const Header = () => {
Portfolio
</Link>
<Link
href={ROUTER_PATH.SETTINGS}
href={ROUTER_PATH.SETTING}
className='text-sm leading-normal font-medium text-text-muted-dark hover:text-white'
>
Settings
Setting
</Link>
</div>
</nav>
Expand Down
1 change: 1 addition & 0 deletions src/shared/components/ui/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export * from "./input";
export * from "./label";
export * from "./select";
export * from "./separator";
export * from "./skeleton";
export * from "./sonner";
export * from "./spinner";
export * from "./table";
Expand Down
7 changes: 7 additions & 0 deletions src/shared/components/ui/skeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { cn } from "../../utils";

function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
return <div data-slot='skeleton' className={cn("animate-pulse rounded-sm bg-text-dark/20", className)} {...props} />;
}

export { Skeleton };
2 changes: 1 addition & 1 deletion src/shared/constants/router-path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ export const ROUTER_PATH = {
AUTO_TRADE: "/auto-trade",
TRADE: "/trade",
PORTFOLIO: "/portfolio",
SETTINGS: "/settings",
SETTING: "/setting",
WALLET: "/wallet",
};
Loading
Loading