Skip to content

Commit 9d6f296

Browse files
Merge remote-tracking branch 'origin/develop'
2 parents f4f27c6 + 1772390 commit 9d6f296

7 files changed

Lines changed: 410 additions & 7 deletions

File tree

package-lock.json

Lines changed: 16 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import { useState } from 'react';
2+
import {
3+
validateAdminId,
4+
validateAdminPassword,
5+
type ValidationResult,
6+
} from '../../../utils/adminValidators';
7+
8+
interface AddAdminFormProps {
9+
onSubmit: (data: { name: string; userId: string; password: string }) => void;
10+
}
11+
12+
const AddAdminForm: React.FC<AddAdminFormProps> = ({ onSubmit }) => {
13+
const [name, setName] = useState('');
14+
const [userId, setUserId] = useState('');
15+
const [password, setPassword] = useState('');
16+
17+
// 유효성 검증 결과 상태
18+
const [userIdValidation, setUserIdValidation] = useState<ValidationResult>({ isValid: true });
19+
const [passwordValidation, setPasswordValidation] = useState<ValidationResult>({ isValid: true });
20+
const [koreanInputError, setKoreanInputError] = useState(false);
21+
22+
// 한글 입력 방지 및 에러 메시지 표시
23+
const handleKoreanInput = (value: string) => {
24+
const hasKorean = /[---]/.test(value);
25+
// 한글이 있으면 에러 메시지 표시, 없으면 바로 숨김
26+
setKoreanInputError(hasKorean);
27+
return hasKorean ? value.replace(/[---]/g, '') : value;
28+
};
29+
30+
// 입력값 변경 핸들러
31+
const handleUserIdChange = (e: React.ChangeEvent<HTMLInputElement>) => {
32+
const value = handleKoreanInput(e.target.value);
33+
setUserId(value);
34+
setUserIdValidation(validateAdminId(value));
35+
};
36+
37+
const handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
38+
const value = handleKoreanInput(e.target.value);
39+
setPassword(value);
40+
setPasswordValidation(validateAdminPassword(value));
41+
};
42+
43+
const isFormValid =
44+
name.trim() &&
45+
userId.trim() &&
46+
password.trim() &&
47+
userIdValidation.isValid &&
48+
passwordValidation.isValid;
49+
50+
const handleSubmit = (e: React.FormEvent) => {
51+
e.preventDefault();
52+
if (isFormValid) {
53+
onSubmit({ name, userId, password });
54+
}
55+
};
56+
57+
return (
58+
<form
59+
onSubmit={handleSubmit}
60+
className="text-white flex flex-col gap-10 max-w-[490px] w-full mx-auto"
61+
>
62+
{/* 이름 */}
63+
<div>
64+
<label className="heading-2-semibold block mb-8">이름</label>
65+
<input
66+
type="text"
67+
placeholder="이름을 입력하세요."
68+
value={name}
69+
onChange={(e) => setName(e.target.value)}
70+
className="w-full rounded-8 bg-grey-700 py-20 px-24 placeholder-grey-400 subhead-1-medium text-grey-50 outline-none focus:ring-1 focus:ring-grey-300"
71+
/>
72+
</div>
73+
74+
{/* 계정 */}
75+
<div>
76+
<label className="heading-2-semibold block mb-8">계정</label>
77+
<div className="mb-12">
78+
<input
79+
type="text"
80+
placeholder="아이디를 입력하세요."
81+
value={userId}
82+
onChange={handleUserIdChange}
83+
pattern="[A-Za-z0-9!@#$%^&*()_+\-=[\]{};':\\|,.<>/?]+"
84+
className={`w-full rounded-8 bg-grey-700 py-20 px-24 placeholder-grey-400 subhead-1-medium text-grey-50 outline-none focus:ring-1 focus:ring-grey-300 ${
85+
!userIdValidation.isValid && userId ? 'ring-1 ring-status-error' : ''
86+
}`}
87+
/>
88+
{!userIdValidation.isValid && userId && (
89+
<p className="mt-8 text-status-error body-2-medium">{userIdValidation.error}</p>
90+
)}
91+
{koreanInputError && (
92+
<p className="mt-8 text-status-error body-2-medium">
93+
한글은 입력할 수 없습니다. 영문, 숫자, 특수문자만 사용 가능합니다.
94+
</p>
95+
)}
96+
</div>
97+
<div>
98+
<input
99+
type="password"
100+
placeholder="비밀번호를 입력하세요."
101+
value={password}
102+
onChange={handlePasswordChange}
103+
pattern="[A-Za-z0-9!@#$%^&*()_+\-=[\]{};':\\|,.<>/?]+"
104+
className={`w-full rounded-8 bg-grey-700 py-20 px-24 placeholder-grey-400 subhead-1-medium text-grey-50 outline-none focus:ring-1 focus:ring-grey-300 ${
105+
!passwordValidation.isValid && password ? 'ring-1 ring-status-error' : ''
106+
}`}
107+
/>
108+
{!passwordValidation.isValid && password && (
109+
<p className="mt-8 text-status-error body-2-medium">{passwordValidation.error}</p>
110+
)}
111+
</div>
112+
</div>
113+
114+
{/* 버튼 */}
115+
<button
116+
type="submit"
117+
disabled={!isFormValid}
118+
className={`w-full mt-48 py-[18px] rounded-8 heading-3-semibold transition-all duration-300
119+
${
120+
isFormValid
121+
? 'text-black cursor-pointer bg-green-300 hover:[background-image:var(--gradient-graphic)]'
122+
: 'bg-grey-200 text-grey-700 cursor-not-allowed opacity-60'
123+
}`}
124+
>
125+
관리자 아이디 추가하기
126+
</button>
127+
</form>
128+
);
129+
};
130+
131+
export default AddAdminForm;
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { useState, useCallback } from 'react';
2+
import AuthDeleteModal from '../auth-manage/AuthDeleteModal';
3+
4+
const AdminList = () => {
5+
const admins = [
6+
{ no: 1, name: '데브톡', id: 'devtalk1' },
7+
{ no: 2, name: '김데브', id: 'adgfeev2' },
8+
{ no: 3, name: '이데브', id: 'deeev3' },
9+
{ no: 4, name: '박데브', id: 'dddev4' },
10+
{ no: 5, name: '최데브', id: 'deeev5' },
11+
{ no: 6, name: '정데브', id: 'kkeev6' },
12+
{ no: 7, name: '안데브', id: 'deeev7' },
13+
{ no: 8, name: '강데브', id: 'deeev8' },
14+
{ no: 9, name: '인데브', id: 'deeev9' },
15+
];
16+
17+
const [modalOpen, setModalOpen] = useState(false);
18+
const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null);
19+
const [selectedAdmin, setSelectedAdmin] = useState<{
20+
no: number;
21+
name: string;
22+
id: string;
23+
} | null>(null);
24+
25+
// 우클릭 메뉴 처리
26+
const handleContextMenu = useCallback((e: React.MouseEvent, admin: typeof selectedAdmin) => {
27+
e.preventDefault(); // 기본 컨텍스트 메뉴 방지
28+
setContextMenu({ x: e.clientX, y: e.clientY });
29+
setSelectedAdmin(admin);
30+
}, []);
31+
32+
const handleDelete = (no: number) => {
33+
console.log(`관리자 ID ${no} 삭제`);
34+
};
35+
36+
return (
37+
<div className="w-full bg-grey-900 text-white rounded-10 overflow-hidden">
38+
<table className="w-full text-left border-collapse">
39+
{/* 헤더 */}
40+
<thead>
41+
<tr className="bg-grey-700 text-grey-200 subhead-1-medium">
42+
<th className="w-[80px] py-20 px-[26px] text-center">No.</th>
43+
<th className="max-w-[415px] py-20 px-24">이름</th>
44+
<th className="py-20 px-24">아이디</th>
45+
<th className="w-[120px] py-20 px-[44px] text-center">관리</th>
46+
</tr>
47+
</thead>
48+
49+
{/* 바디 */}
50+
<tbody>
51+
{admins.map((admin) => (
52+
<tr
53+
key={admin.no}
54+
onContextMenu={(e) => handleContextMenu(e, admin)}
55+
className="subhead-1-medium border-t border-grey-700 hover:bg-grey-800 transition-colors"
56+
>
57+
<td className="py-20 px-20 text-center">{String(admin.no).padStart(2, '0')}</td>
58+
<td className="py-20 px-24">{admin.name}</td>
59+
<td className="py-20 px-24">{admin.id}</td>
60+
<td
61+
className="py-20 px-[44px] text-center text-status-error hover:text-shadow-status-error cursor-pointer"
62+
onClick={() => {
63+
setSelectedAdmin(admin);
64+
setModalOpen(true);
65+
}}
66+
>
67+
삭제
68+
</td>
69+
</tr>
70+
))}
71+
</tbody>
72+
</table>
73+
74+
{selectedAdmin && (
75+
<AuthDeleteModal
76+
open={modalOpen}
77+
adminId={selectedAdmin?.id ?? ''}
78+
adminName={selectedAdmin?.name ?? ''}
79+
onConfirm={() => {
80+
handleDelete(selectedAdmin?.no ?? 0);
81+
setModalOpen(false);
82+
}}
83+
onCancel={() => setModalOpen(false)}
84+
/>
85+
)}
86+
87+
{/* 우클릭 메뉴 */}
88+
{contextMenu && selectedAdmin && (
89+
<div
90+
className="fixed bg-grey-800 rounded-8 shadow-lg py-8 z-50 hover:bg-grey-700"
91+
style={{
92+
left: contextMenu.x,
93+
top: contextMenu.y,
94+
}}
95+
>
96+
<button
97+
className="w-full px-16 py-12 text-left text-status-error body-2-medium cursor-pointer"
98+
onClick={() => {
99+
setModalOpen(true);
100+
setContextMenu(null);
101+
}}
102+
>
103+
관리자 권한 삭제
104+
</button>
105+
</div>
106+
)}
107+
108+
{/* 배경 클릭시 컨텍스트 메뉴 닫기 */}
109+
{contextMenu && <div className="fixed inset-0 z-40" onClick={() => setContextMenu(null)} />}
110+
</div>
111+
);
112+
};
113+
114+
export default AdminList;

0 commit comments

Comments
 (0)