Skip to content

Commit 7320e02

Browse files
authored
feat(마이페이지): 내 정보 조회 및 수정 페이지 UI 통일 (#60)
1 parent a10760c commit 7320e02

File tree

18 files changed

+214
-210
lines changed

18 files changed

+214
-210
lines changed

frontend/.vscode/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,8 @@
99
"editor.defaultFormatter": "biomejs.biome",
1010
"editor.codeActionsOnSave": {
1111
"quickfix.biome": "explicit"
12+
},
13+
"[typescript]": {
14+
"editor.defaultFormatter": "biomejs.biome"
1215
}
1316
}

frontend/packages/ui/src/components/Input/Input.stories.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import type { Meta, StoryObj } from '@storybook/react';
2-
import { fn } from '@storybook/test';
32
import { Input } from './Input';
43

54
const meta = {
@@ -11,9 +10,10 @@ const meta = {
1110
tags: ['autodocs'],
1211
argTypes: {},
1312
args: {
14-
onClickSearch: fn(),
15-
type: 'search',
16-
placeholder: '검색어를 입력하세요',
13+
id: 'nickname',
14+
type: 'text',
15+
placeholder: '크레코',
16+
label: '닉네임',
1717
},
1818
} satisfies Meta<typeof Input>;
1919

frontend/packages/ui/src/components/Input/Input.tsx

Lines changed: 19 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,28 @@
1-
'use client';
2-
31
import { cn } from '@ui/lib/utils';
4-
import React, { useState } from 'react';
5-
import { Icon } from '../Icon';
2+
import React from 'react';
63

7-
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
8-
onClickSearch?: (value: string) => void;
9-
}
4+
const labelStyle = "font-['Roboto'] font-normal block text-left text-[14px] leading-[22px]";
105

11-
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
12-
({ className, type, onClickSearch, ...props }, ref) => {
13-
const [value, setValue] = useState<string>('');
6+
const inputStyle =
7+
'w-full box-border flex-row align-center p-[13px] gap-[10px] bg-[#FFFFFF] border-[1px] border-[#C6C6C6] rounded-[20px]';
148

15-
if (type === 'search') {
16-
props.placeholder = '검색어를 입력하세요';
17-
}
9+
interface Props extends React.InputHTMLAttributes<HTMLInputElement> {
10+
type?: string;
11+
label?: string;
12+
className?: string;
13+
inputClassName?: string;
14+
}
1815

16+
export const Input = React.forwardRef<HTMLInputElement, Props>(
17+
(
18+
{ id, type, className, disabled, required, defaultValue, label, placeholder, inputClassName = '', ...props }: Props,
19+
ref,
20+
) => {
1921
return (
20-
<div className="relative" style={{ width: props.width ? `${props.width}px` : '200px' }}>
21-
<input
22-
ref={ref}
23-
type={type}
24-
className={cn(
25-
'flex h-[44px] w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 pr-[44px]',
26-
className,
27-
)}
28-
onChange={(e) => setValue(e.target.value)}
29-
value={value}
30-
{...props}
31-
/>
32-
{type === 'search' ? (
33-
<Icon
34-
name="search"
35-
className="absolute top-0 right-0 mr-4 mt-[10px]"
36-
onClick={() => onClickSearch?.(value)}
37-
/>
38-
) : null}
39-
</div>
22+
<p className={cn(className, 'mb-[15px]')}>
23+
{label && <label className={labelStyle}>{label}</label>}
24+
<input ref={ref} id={id} className={cn(inputStyle, inputClassName)} type={type} {...props} />
25+
</p>
4026
);
4127
},
4228
);
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import type { Meta, StoryObj } from '@storybook/react';
2+
import { fn } from '@storybook/test';
3+
import { SearchInput } from './SearchInput';
4+
5+
const meta = {
6+
title: 'Zicdding-UI/SearchInput',
7+
component: SearchInput,
8+
parameters: {
9+
layout: 'centered',
10+
},
11+
tags: ['autodocs'],
12+
argTypes: {},
13+
args: {
14+
onClickSearch: fn(),
15+
type: 'search',
16+
placeholder: '검색어를 입력하세요',
17+
},
18+
} satisfies Meta<typeof SearchInput>;
19+
20+
export default meta;
21+
22+
type Story = StoryObj<typeof meta>;
23+
24+
export const Default: Story = {
25+
args: {},
26+
};
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
'use client';
2+
3+
import { cn } from '@ui/lib/utils';
4+
import React, { useState } from 'react';
5+
import { Icon } from '../Icon';
6+
7+
export interface SearchInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
8+
onClickSearch?: (value: string) => void;
9+
}
10+
11+
export const SearchInput = React.forwardRef<HTMLInputElement, SearchInputProps>(
12+
({ className, onClickSearch, ...props }, ref) => {
13+
const [value, setValue] = useState<string>('');
14+
15+
return (
16+
<div className="relative" style={{ width: props.width ? `${props.width}px` : '200px' }}>
17+
<input
18+
ref={ref}
19+
type="search"
20+
className={cn(
21+
'flex h-[44px] w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 pr-[44px]',
22+
className,
23+
)}
24+
onChange={(e) => setValue(e.target.value)}
25+
value={value}
26+
placeholder="검색어를 입력하세요"
27+
{...props}
28+
/>
29+
<Icon name="search" className="absolute top-0 right-0 mr-4 mt-[10px]" onClick={() => onClickSearch?.(value)} />
30+
</div>
31+
);
32+
},
33+
);
34+
35+
SearchInput.displayName = 'SearchInput';
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
export { SearchInput } from './SearchInput';
12
export { Input } from './Input';

frontend/packages/ui/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
export * from './components/Button/Button';
2-
export * from './components/Input/Input';
2+
export * from './components/Input/SearchInput';
33
export * from './components/Typography/Typography';
44
export * from './components/Calendar/Calendar';
55
export * from './components/Card/Card';

frontend/zicdding-class.com/app/(test)/test/input/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client';
22

3-
import { Input } from '@zicdding-web/ui/Input';
3+
import { SearchInput } from '@zicdding-web/ui/Input';
44
import { useState } from 'react';
55

66
export default function TestPage() {
@@ -10,7 +10,7 @@ export default function TestPage() {
1010
<h1>테스트 페이지</h1>
1111
<div>
1212
입력된 값: {value}
13-
<Input width="200" type="search" value={value} onChange={(e) => setValue(e.target.value)} />
13+
<SearchInput width="200" type="search" value={value} onChange={(e: any) => setValue(e.target.value)} />
1414
</div>
1515
</>
1616
);
Lines changed: 77 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,84 +1,101 @@
11
'use client';
22

33
import { useUser } from '@/app/_hooks';
4+
import { Button } from '@zicdding-web/ui';
5+
import { Input } from '@zicdding-web/ui/Input';
46
import { useRouter } from 'next/navigation';
57
import { useEffect } from 'react';
8+
import { useForm } from 'react-hook-form';
69

7-
export function MyInfo() {
8-
const { user, isLogged } = useUser();
10+
export function MyInfo({ mode }: { mode: 'view' | 'modify' }) {
911
const router = useRouter();
12+
const { user } = useUser();
13+
const { register, setValue } = useForm<{
14+
nickname: string;
15+
email: string;
16+
password: string;
17+
phoneNumber: string;
18+
}>();
1019

1120
useEffect(() => {
12-
if (!isLogged) {
13-
router.push('/login');
21+
if (user == null) {
22+
return;
1423
}
15-
}, [isLogged, router]);
24+
25+
setValue('nickname', user.nickname);
26+
setValue('email', user.email);
27+
setValue('phoneNumber', user.phone_num);
28+
}, [user, setValue]);
1629

1730
return (
1831
<div className="space-y-4 w-[420px] mx-auto my-0">
19-
<div className="grid grid-cols-2 gap-4 items-center">
20-
<label htmlFor="nickname" className="text-gray-700 font-bold">
21-
닉네임
22-
</label>
23-
<input
24-
id="nickname"
25-
type="text"
26-
value={user?.nickname || ''}
27-
readOnly
28-
className="form-input w-full border-gray-300"
29-
/>
30-
</div>
32+
<div className="w-[200px] flex justify-center ml-auto mr-auto mb-8 flex-col">
33+
<img src="/my/default-profile.png" alt="Profile" width="205" className="bg-gray-200 mb-10" />
3134

32-
<div className="grid grid-cols-2 gap-4 items-center">
33-
<label htmlFor="email" className="text-gray-700 font-bold">
34-
Email
35-
</label>
36-
<input
37-
id="email"
38-
type="email"
39-
value={user?.email || ''}
40-
readOnly
41-
className="form-input w-full border-gray-300"
42-
/>
35+
<Button className="w-full rounded-[20px]" onClick={() => alert('업로드 버튼이 눌렸습니다.')}>
36+
업로드
37+
</Button>
4338
</div>
4439

45-
<div className="grid grid-cols-2 gap-4 items-center">
46-
<label htmlFor="password" className="text-gray-700 font-bold">
47-
비밀번호
48-
</label>
49-
<input
50-
id="password"
51-
type="password"
52-
value={isLogged ? '********' : ''}
53-
readOnly
54-
className="form-input w-full border-gray-300"
55-
/>
56-
</div>
40+
<Input
41+
id="nickname"
42+
label="닉네임"
43+
disabled={mode === 'view'}
44+
required={true}
45+
inputClassName={mode === 'view' ? 'text-[#959595] bg-[#F4F4F4]' : 'bg-[#F4F4F4] text-[#959595]'}
46+
{...register('nickname')}
47+
/>
5748

58-
<div className="grid grid-cols-2 gap-4 items-center">
59-
<label htmlFor="phoneNumber" className="text-gray-700 font-bold">
60-
전화번호
61-
</label>
62-
<input
63-
id="phoneNumber"
64-
type="tel"
65-
value={user?.phone_num || ''}
66-
readOnly
67-
className="form-input w-full border-gray-300"
68-
/>
69-
</div>
49+
<Input
50+
label="Email"
51+
id="email"
52+
type="email"
53+
disabled={true}
54+
inputClassName={mode === 'view' ? 'text-[#959595] bg-[#F4F4F4]' : ''}
55+
{...register('email')}
56+
/>
7057

71-
<div className="h-[80px]">{}</div>
58+
<Input
59+
defaultValue="**********"
60+
label="비밀번호"
61+
id="password"
62+
type="password"
63+
disabled={true}
64+
inputClassName={mode === 'view' ? 'text-[#959595] bg-[#F4F4F4]' : ''}
65+
{...register('password')}
66+
/>
7267

73-
<div className="flex justify-center ">
74-
<button
75-
type="button"
76-
className="bg-black text-white py-2 px-4 rounded font-bold text-lg w-[200px]"
77-
onClick={() => router.push('/my/modify')}
68+
<Input
69+
label="전화번호"
70+
id="phoneNumber"
71+
type="tel"
72+
disabled={mode === 'view'}
73+
{...register('phoneNumber')}
74+
inputClassName={mode === 'view' ? 'text-[#959595] bg-[#F4F4F4]' : 'bg-[#F4F4F4]'}
75+
placeholder="010-0000-0000"
76+
/>
77+
78+
<div className="mb-20 w-full h-1" />
79+
80+
{mode === 'modify' ? (
81+
<div className="flex justify-evenly">
82+
<Button className="w-full mr-8 rounded-[20px]" onClick={() => alert('수정 버튼이 눌려졌습니다.')}>
83+
수정하기
84+
</Button>
85+
<Button className="w-full rounded-[20px]" onClick={() => router.back()}>
86+
취소
87+
</Button>
88+
</div>
89+
) : (
90+
<Button
91+
className="w-full rounded-[20px]"
92+
onClick={() => {
93+
router.push('/my/modify');
94+
}}
7895
>
7996
개인정보 수정
80-
</button>
81-
</div>
97+
</Button>
98+
)}
8299
</div>
83100
);
84101
}

0 commit comments

Comments
 (0)