-
Notifications
You must be signed in to change notification settings - Fork 0
사용자 정보 수정 페이지 구현 #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
사용자 정보 수정 페이지 구현 #11
Changes from all commits
5f0cfc8
7d8d36c
e58fb7b
aac3546
53d155b
f84c333
06de521
0dc4237
c2044f9
6109a70
edaae32
5bae757
c445c2d
b3f04b8
52858f5
026c151
625de44
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -43,4 +43,5 @@ next-env.d.ts | |
| # local env files | ||
| .env.local | ||
| /.vscode | ||
| /docs | ||
| /docs | ||
| /.github/copilot-instructions.md | ||
| 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
+18
to
+28
|
||||||
|
|
||||||
| // 사용자 메타데이터 업데이트 | ||||||
| const { data, error } = await supabase.auth.updateUser({ | ||||||
| data: { | ||||||
| nickname, | ||||||
| }, | ||||||
| }); | ||||||
|
Comment on lines
+30
to
+35
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||
|
|
||||||
| 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 } }, | ||||||
|
||||||
| { status: "success", data: { nickname: data.user.user_metadata.nickname } }, | |
| { status: "success", data: { nickname: data?.user?.user_metadata?.nickname } }, |
| 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> | ||
| ); | ||
| } |
| 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"; |
| 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
|
||||||||||||||||||||||||||
| 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); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export * from "./change-nickname.api"; |
| 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> | ||
| ); | ||
| }; |
| 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'> | ||||||
|
||||||
| <div className='flex items-center gap-4'> | |
| <div className='flex items-center gap-2'> |
Copilot
AI
Nov 22, 2025
There was a problem hiding this comment.
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.
| <Label className='text-text-primary w-20'>마지막 접속일</Label> | |
| <Label className='text-text-primary w-16'>마지막 접속일</Label> |
| 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
|
||||||||||||||||||||||||||||||||||||||||||
| <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
AI
Nov 22, 2025
There was a problem hiding this comment.
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.
| {!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>} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| export * from "./EmailField"; | ||
| export * from "./LastLoginField"; | ||
| export * from "./NicknameField"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export * from "./features"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| export * from "./useUserInfo"; | ||
| export * from "./useChangeNickname"; |
| 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 }), | ||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. API로 닉네임을 보내기 전에
Suggested change
|
||||||||||||||||||||||||||||||||||||||
| 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) { | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
| if (inputValue.trim() === userNickname) { | |
| if (inputValue.trim() === userNickname.trim()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
사용자 경험을 개선하기 위해 닉네임 변경 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");
}
};
Copilot
AI
Nov 22, 2025
There was a problem hiding this comment.
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
AI
Nov 22, 2025
There was a problem hiding this comment.
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).
| return { | |
| fieldState, | |
| inputValue, | |
| onChangeNickname, | |
| onClickEditButton, | |
| const onCancelEdit = () => { | |
| setInputValue(userNickname); | |
| setFieldState("view"); | |
| }; | |
| return { | |
| fieldState, | |
| inputValue, | |
| onChangeNickname, | |
| onClickEditButton, | |
| onCancelEdit, |
| 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 }; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export * from "./ui"; |
| 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' | ||||||||
|
||||||||
| 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="프로필 사진 업로드" |
| 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> | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| export * from "./ProfileSection"; | ||
| export * from "./UserInfoSection"; |
| 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
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
formattedDate 함수에 몇 가지 개선 사항을 제안합니다.
- 버그 수정:
dateString이 비어있을 때"년 월 일 : :"와 같은 잘못된 형식의 문자열이 반환됩니다. 유효하지 않은 입력에 대해서는"-"같은 대체 텍스트를 반환하도록 수정해야 합니다. - 유효성 검사 강화:
new Date(dateString)이Invalid Date를 반환하는 경우에 대한 처리를 추가하여 안정성을 높일 수 있습니다. - 네이밍 컨벤션: 변수명
Hour,Minutes,Seconds를 JavaScript/TypeScript 표준인 camelCase (hour,minutes,seconds)로 변경하여 코드 스타일의 일관성을 맞추는 것이 좋습니다.
| 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}`; | |
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export * from "./formatted-date"; |
| 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 }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
닉네임에 대한 유효성 검사를 강화하는 것을 제안합니다. 현재 코드는 앞뒤 공백이 포함된 닉네임을 허용할 수 있습니다.
trim()을 사용하여 입력값의 양 끝에 있는 공백을 제거한 후 유효성을 검사하는 것이 좋습니다. 이렇게 하면 " test "와 같은 닉네임이 데이터베이스에 저장되는 것을 방지할 수 있습니다.