Skip to content
158 changes: 158 additions & 0 deletions src/app/preview/modal/Some.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { useState } from 'react';
import React from 'react';

import { Button } from '../../../components/ui/Button';
import Modal from '../../../components/ui/modal/Modal';

interface UserData {
id: number;
name: string;
status: 'λŒ€κΈ°' | '승인' | '거절' | '강퇴';
introduction: string;
}

const mockUsers: UserData[] = [
{
id: 1,
name: 'κΉ€λ―Όμˆ˜',
status: '승인',
introduction:
'μ•ˆλ…•ν•˜μ„Έμš”! μ›Ή 개발자 κΉ€λ―Όμˆ˜μž…λ‹ˆλ‹€. React와 TypeScriptλ₯Ό 주둜 μ‚¬μš©ν•©λ‹ˆλ‹€.',
},
{
id: 2,
name: '이지원',
status: 'λŒ€κΈ°',
introduction: 'λ°±μ—”λ“œ 개발자 μ΄μ§€μ›μž…λ‹ˆλ‹€. Springκ³Ό Node.jsλ₯Ό λ‹€λ£Ήλ‹ˆλ‹€.',
},
{
id: 3,
name: 'λ°•μ„œμ—°',
status: '거절',
introduction:
'UI/UX λ””μžμ΄λ„ˆ λ°•μ„œμ—°μž…λ‹ˆλ‹€. μ‚¬μš©μž κ²½ν—˜ κ°œμ„ μ— 관심이 λ§ŽμŠ΅λ‹ˆλ‹€.',
},
{
id: 4,
name: 'μ΅œμ€€ν˜Έ',
status: '승인',
introduction:
'λͺ¨λ°”일 μ•± 개발자 μ΅œμ€€ν˜Έμž…λ‹ˆλ‹€. Flutter와 React Nativeλ₯Ό μ‚¬μš©ν•©λ‹ˆλ‹€.',
},
{
id: 5,
name: '정닀은',
status: '강퇴',
introduction:
'ν”„λ‘ νŠΈμ—”λ“œ 개발자 μ •λ‹€μ€μž…λ‹ˆλ‹€. Vue.js와 Reactλ₯Ό 주둜 μ‚¬μš©ν•©λ‹ˆλ‹€.',
},
{
id: 6,
name: 'κ°•ν˜„μš°',
status: '승인',
introduction: 'ν’€μŠ€νƒ 개발자 κ°•ν˜„μš°μž…λ‹ˆλ‹€. MERN μŠ€νƒμ„ 주둜 μ‚¬μš©ν•©λ‹ˆλ‹€.',
},
{
id: 7,
name: 'μ†λ―Έλ‚˜',
status: 'λŒ€κΈ°',
introduction: 'DevOps μ—”μ§€λ‹ˆμ–΄ μ†λ―Έλ‚˜μž…λ‹ˆλ‹€. AWS와 Dockerλ₯Ό λ‹€λ£Ήλ‹ˆλ‹€.',
},
{
id: 8,
name: 'μœ€νƒœν˜Έ',
status: '거절',
introduction: 'κ²Œμž„ 개발자 μœ€νƒœν˜Έμž…λ‹ˆλ‹€. Unity와 C#을 주둜 μ‚¬μš©ν•©λ‹ˆλ‹€.',
},
{
id: 9,
name: 'μž„μˆ˜μ§„',
status: '승인',
introduction: '데이터 μ—”μ§€λ‹ˆμ–΄ μž„μˆ˜μ§„μž…λ‹ˆλ‹€. Pythonκ³Ό SQL을 λ‹€λ£Ήλ‹ˆλ‹€.',
},
{
id: 10,
name: 'ν•œλ„μœ€',
status: 'λŒ€κΈ°',
introduction:
'λ³΄μ•ˆ μ—”μ§€λ‹ˆμ–΄ ν•œλ„μœ€μž…λ‹ˆλ‹€. λ„€νŠΈμ›Œν¬ λ³΄μ•ˆμ— 관심이 λ§ŽμŠ΅λ‹ˆλ‹€.',
},
];

const Some = () => {
const [isSecondModalOpen, setIsSecondModalOpen] = useState(false);
const [selectedUser, setSelectedUser] = useState<UserData | null>(null);

Comment on lines +82 to +85
Copy link

Choose a reason for hiding this comment

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

πŸ› οΈ Refactor suggestion

μ»΄ν¬λ„ŒνŠΈ 이름 및 μƒνƒœ 관리 κ°œμ„  ν•„μš”

μ»΄ν¬λ„ŒνŠΈ 이름 'Some'이 의미λ₯Ό λͺ…ν™•νžˆ μ „λ‹¬ν•˜μ§€ λͺ»ν•˜λ©°, μƒνƒœ λ³€μˆ˜λͺ…도 직관적이지 μ•ŠμŠ΅λ‹ˆλ‹€.

λ‹€μŒκ³Ό 같이 μˆ˜μ •ν•˜μ„Έμš”:

-const Some = () => {
-  const [isSecondModalOpen, setIsSecondModalOpen] = useState(false);
-  const [selectedUser, setSelectedUser] = useState<UserData | null>(null);
+const UserProfileList = () => {
+  const [isProfileModalOpen, setIsProfileModalOpen] = useState(false);
+  const [selectedUser, setSelectedUser] = useState<UserData | null>(null);
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const Some = () => {
const [isSecondModalOpen, setIsSecondModalOpen] = useState(false);
const [selectedUser, setSelectedUser] = useState<UserData | null>(null);
const UserProfileList = () => {
const [isProfileModalOpen, setIsProfileModalOpen] = useState(false);
const [selectedUser, setSelectedUser] = useState<UserData | null>(null);

const handleSecondModalConfirm = () => {
setIsSecondModalOpen(false);
};

const getStatusColor = (status: string) => {
switch (status) {
case '승인':
return 'text-green-600';
case 'λŒ€κΈ°':
return 'text-yellow-600';
case '거절':
return 'text-red-600';
case '강퇴':
return 'text-gray-600';
default:
return '';
}
};

const handleProfileClick = (user: UserData) => {
setSelectedUser(user);
setIsSecondModalOpen(true);
};

return (
<div className="flex flex-col space-y-4">
<div className="grid grid-cols-4 gap-4 p-2 font-semibold">
<div className="text-white">이름</div>
<div className="text-white">μƒνƒœ</div>
<div className="text-white">ν”„λ‘œν•„</div>
</div>
<div className="w-full border-t border-white"></div>
{mockUsers.map((user) => (
<div
key={user.id}
className="grid grid-cols-4 items-center gap-4 border-b pb-2"
>
<div className="text-white">{user.name}</div>
<div className={getStatusColor(user.status)}>{user.status}</div>
<div>
<Button
onClick={() => handleProfileClick(user)}
variant="outline"
size="sm"
>
ν”„λ‘œν•„ 보기
</Button>
</div>
</div>
))}
Comment on lines +111 to +135
Copy link

Choose a reason for hiding this comment

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

πŸ› οΈ Refactor suggestion

κ·Έλ¦¬λ“œ λ ˆμ΄μ•„μ›ƒ 및 μ ‘κ·Όμ„± κ°œμ„  ν•„μš”

ν…Œμ΄λΈ” ν˜•νƒœμ˜ 데이터λ₯Ό div둜 κ΅¬ν˜„ν•˜λ©΄ 슀크린 리더 μ‚¬μš©μžκ°€ 데이터 κ°„μ˜ 관계λ₯Ό μ΄ν•΄ν•˜κΈ° μ–΄λ ΅μŠ΅λ‹ˆλ‹€.

μ‹œλ§¨ν‹± ν…Œμ΄λΈ” λ§ˆν¬μ—…μ„ μ‚¬μš©ν•˜λ„λ‘ μˆ˜μ •ν•˜μ„Έμš”:

-<div className="flex flex-col space-y-4">
-  <div className="grid grid-cols-4 gap-4 p-2 font-semibold">
-    <div className="text-white">이름</div>
-    <div className="text-white">μƒνƒœ</div>
-    <div className="text-white">ν”„λ‘œν•„</div>
-  </div>
+<div className="flex flex-col space-y-4">
+  <table className="w-full">
+    <thead>
+      <tr className="grid grid-cols-4 gap-4 p-2 font-semibold">
+        <th scope="col" className="text-white text-left">이름</th>
+        <th scope="col" className="text-white text-left">μƒνƒœ</th>
+        <th scope="col" className="text-white text-left">ν”„λ‘œν•„</th>
+      </tr>
+    </thead>
+    <tbody>

Committable suggestion skipped: line range outside the PR's diff.


<Modal
isOpen={isSecondModalOpen}
onClose={() => setIsSecondModalOpen(false)}
onConfirm={handleSecondModalConfirm}
confirmText="확인"
cancelText="λ‹«κΈ°"
modalClassName="w-96"
>
{selectedUser && (
<div className="p-4">
<h2 className="mb-4 text-xl font-bold text-white">
{selectedUser.name}λ‹˜μ˜ ν”„λ‘œν•„
</h2>
<p className="text-gray-600">{selectedUser.introduction}</p>
</div>
)}
</Modal>
Comment on lines +137 to +153
Copy link

Choose a reason for hiding this comment

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

πŸ› οΈ Refactor suggestion

λͺ¨λ‹¬ λ‚΄μš©μ˜ μ ‘κ·Όμ„± κ°œμ„  ν•„μš”

λͺ¨λ‹¬ λ‚΄μš©μ˜ ꡬ쑰와 μ ‘κ·Όμ„± 속성이 λ―Έν‘ν•©λ‹ˆλ‹€.

λ‹€μŒκ³Ό 같이 κ°œμ„ ν•˜μ„Έμš”:

 <Modal
   isOpen={isSecondModalOpen}
   onClose={() => setIsSecondModalOpen(false)}
   onConfirm={handleSecondModalConfirm}
   confirmText="확인"
   cancelText="λ‹«κΈ°"
   modalClassName="w-96"
+  aria-labelledby="profile-modal-title"
+  aria-describedby="profile-modal-content"
 >
   {selectedUser && (
     <div className="p-4">
       <h2 
+        id="profile-modal-title"
         className="mb-4 text-xl font-bold text-white"
       >
         {selectedUser.name}λ‹˜μ˜ ν”„λ‘œν•„
       </h2>
-      <p className="text-gray-600">{selectedUser.introduction}</p>
+      <p 
+        id="profile-modal-content"
+        className="text-gray-600"
+      >
+        {selectedUser.introduction}
+      </p>
     </div>
   )}
 </Modal>
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Modal
isOpen={isSecondModalOpen}
onClose={() => setIsSecondModalOpen(false)}
onConfirm={handleSecondModalConfirm}
confirmText="확인"
cancelText="λ‹«κΈ°"
modalClassName="w-96"
>
{selectedUser && (
<div className="p-4">
<h2 className="mb-4 text-xl font-bold text-white">
{selectedUser.name}λ‹˜μ˜ ν”„λ‘œν•„
</h2>
<p className="text-gray-600">{selectedUser.introduction}</p>
</div>
)}
</Modal>
<Modal
isOpen={isSecondModalOpen}
onClose={() => setIsSecondModalOpen(false)}
onConfirm={handleSecondModalConfirm}
confirmText="확인"
cancelText="λ‹«κΈ°"
modalClassName="w-96"
aria-labelledby="profile-modal-title"
aria-describedby="profile-modal-content"
>
{selectedUser && (
<div className="p-4">
<h2
id="profile-modal-title"
className="mb-4 text-xl font-bold text-white"
>
{selectedUser.name}λ‹˜μ˜ ν”„λ‘œν•„
</h2>
<p
id="profile-modal-content"
className="text-gray-600"
>
{selectedUser.introduction}
</p>
</div>
)}
</Modal>

</div>
);
};

export default Some;
56 changes: 56 additions & 0 deletions src/app/preview/modal/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
'use client';

import Some from '@/app/preview/modal/Some';
import { Button } from '@/components/ui/Button';
import Modal from '@/components/ui/modal/Modal';
import { useState } from 'react';

export default function ModalTestPage() {
const [isModalOpen, setIsModalOpen] = useState(false);
const [isModalOpen2, setIsModalOpen2] = useState(false);
const [isModalOpen3, setIsModalOpen3] = useState(false); // μƒˆλ‘œμš΄ μƒνƒœ μΆ”κ°€

const handleConfirm = () => {
setIsModalOpen(false);
};

return (
<div className="flex min-h-screen flex-col items-center justify-center gap-4 bg-BG p-4">
<Button onClick={() => setIsModalOpen(true)}>이쀑 λͺ¨λ‹¬</Button>
<Button onClick={() => setIsModalOpen2(true)}>κ·Έλƒ₯ λͺ¨λ‹¬</Button>
<Button onClick={() => setIsModalOpen3(true)}>λ‹«κΈ°λ§Œ μžˆλŠ” λͺ¨λ‹¬</Button>

<Modal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
onConfirm={handleConfirm}
confirmText="μ‚­μ œ"
cancelText="μ·¨μ†Œ"
modalClassName="w-96"
>
<Some />
</Modal>
Comment on lines +23 to +32
Copy link

Choose a reason for hiding this comment

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

πŸ› οΈ Refactor suggestion

λͺ¨λ‹¬μ˜ μ ‘κ·Όμ„± κ°œμ„  ν•„μš”

λͺ¨λ‹¬μ— 제λͺ©μ„ λͺ…μ‹œμ μœΌλ‘œ μ—°κ²°ν•˜λŠ” aria-labelledby 속성이 λˆ„λ½λ˜μ—ˆμŠ΅λ‹ˆλ‹€.

λ‹€μŒκ³Ό 같이 μˆ˜μ •ν•˜μ„Έμš”:

 <Modal
   isOpen={isModalOpen}
   onClose={() => setIsModalOpen(false)}
   onConfirm={handleConfirm}
   confirmText="μ‚­μ œ"
   cancelText="μ·¨μ†Œ"
   modalClassName="w-96"
+  aria-labelledby="first-modal-title"
 >
+  <h2 id="first-modal-title" className="sr-only">이쀑 λͺ¨λ‹¬</h2>
   <Some />
 </Modal>
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Modal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
onConfirm={handleConfirm}
confirmText="μ‚­μ œ"
cancelText="μ·¨μ†Œ"
modalClassName="w-96"
>
<Some />
</Modal>
<Modal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
onConfirm={handleConfirm}
confirmText="μ‚­μ œ"
cancelText="μ·¨μ†Œ"
modalClassName="w-96"
aria-labelledby="first-modal-title"
>
<h2 id="first-modal-title" className="sr-only">이쀑 λͺ¨λ‹¬</h2>
<Some />
</Modal>


<Modal
isOpen={isModalOpen2}
onClose={() => setIsModalOpen2(false)}
onConfirm={handleConfirm}
confirmText="μ‚­μ œ"
cancelText="μ·¨μ†Œ"
modalClassName="w-96"
>
<p className="text-white">μ•ˆλ…•ν•˜μ„Έμš©μš©</p>
</Modal>
Comment on lines +34 to +43
Copy link

Choose a reason for hiding this comment

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

πŸ› οΈ Refactor suggestion

두 번째 λͺ¨λ‹¬μ˜ μ ‘κ·Όμ„± κ°œμ„  ν•„μš”

λͺ¨λ‹¬μ— 제λͺ©κ³Ό μ„€λͺ…을 λͺ…μ‹œμ μœΌλ‘œ μ—°κ²°ν•˜λŠ” ARIA 속성이 ν•„μš”ν•©λ‹ˆλ‹€.

λ‹€μŒκ³Ό 같이 μˆ˜μ •ν•˜μ„Έμš”:

 <Modal
   isOpen={isModalOpen2}
   onClose={() => setIsModalOpen2(false)}
   onConfirm={handleConfirm}
   confirmText="μ‚­μ œ"
   cancelText="μ·¨μ†Œ"
   modalClassName="w-96"
+  aria-labelledby="second-modal-title"
+  aria-describedby="second-modal-content"
 >
+  <h2 id="second-modal-title" className="sr-only">κΈ°λ³Έ λͺ¨λ‹¬</h2>
-  <p className="text-white">μ•ˆλ…•ν•˜μ„Έμš©μš©</p>
+  <p id="second-modal-content" className="text-white">μ•ˆλ…•ν•˜μ„Έμš©μš©</p>
 </Modal>
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Modal
isOpen={isModalOpen2}
onClose={() => setIsModalOpen2(false)}
onConfirm={handleConfirm}
confirmText="μ‚­μ œ"
cancelText="μ·¨μ†Œ"
modalClassName="w-96"
>
<p className="text-white">μ•ˆλ…•ν•˜μ„Έμš©μš©</p>
</Modal>
<Modal
isOpen={isModalOpen2}
onClose={() => setIsModalOpen2(false)}
onConfirm={handleConfirm}
confirmText="μ‚­μ œ"
cancelText="μ·¨μ†Œ"
modalClassName="w-96"
aria-labelledby="second-modal-title"
aria-describedby="second-modal-content"
>
<h2 id="second-modal-title" className="sr-only">κΈ°λ³Έ λͺ¨λ‹¬</h2>
<p id="second-modal-content" className="text-white">μ•ˆλ…•ν•˜μ„Έμš©μš©</p>
</Modal>


<Modal
isOpen={isModalOpen3}
onClose={() => setIsModalOpen3(false)}
closeOnly
cancelText="λ‹«κΈ°"
modalClassName="w-96"
>
<p className="text-white">λ‹«κΈ° λ²„νŠΌλ§Œ μžˆλŠ” λͺ¨λ‹¬μž…λ‹ˆλ‹€!</p>
</Modal>
Comment on lines +45 to +53
Copy link

Choose a reason for hiding this comment

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

πŸ› οΈ Refactor suggestion

μ„Έ 번째 λͺ¨λ‹¬μ˜ μ ‘κ·Όμ„± κ°œμ„  ν•„μš”

λ‹«κΈ° μ „μš© λͺ¨λ‹¬μ—λ„ 제λͺ©κ³Ό μ„€λͺ…을 λͺ…μ‹œμ μœΌλ‘œ μ—°κ²°ν•˜λŠ” ARIA 속성이 ν•„μš”ν•©λ‹ˆλ‹€.

λ‹€μŒκ³Ό 같이 μˆ˜μ •ν•˜μ„Έμš”:

 <Modal
   isOpen={isModalOpen3}
   onClose={() => setIsModalOpen3(false)}
   closeOnly
   cancelText="λ‹«κΈ°"
   modalClassName="w-96"
+  aria-labelledby="third-modal-title"
+  aria-describedby="third-modal-content"
 >
+  <h2 id="third-modal-title" className="sr-only">λ‹«κΈ° μ „μš© λͺ¨λ‹¬</h2>
-  <p className="text-white">λ‹«κΈ° λ²„νŠΌλ§Œ μžˆλŠ” λͺ¨λ‹¬μž…λ‹ˆλ‹€!</p>
+  <p id="third-modal-content" className="text-white">λ‹«κΈ° λ²„νŠΌλ§Œ μžˆλŠ” λͺ¨λ‹¬μž…λ‹ˆλ‹€!</p>
 </Modal>
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Modal
isOpen={isModalOpen3}
onClose={() => setIsModalOpen3(false)}
closeOnly
cancelText="λ‹«κΈ°"
modalClassName="w-96"
>
<p className="text-white">λ‹«κΈ° λ²„νŠΌλ§Œ μžˆλŠ” λͺ¨λ‹¬μž…λ‹ˆλ‹€!</p>
</Modal>
<Modal
isOpen={isModalOpen3}
onClose={() => setIsModalOpen3(false)}
closeOnly
cancelText="λ‹«κΈ°"
modalClassName="w-96"
aria-labelledby="third-modal-title"
aria-describedby="third-modal-content"
>
<h2 id="third-modal-title" className="sr-only">λ‹«κΈ° μ „μš© λͺ¨λ‹¬</h2>
<p id="third-modal-content" className="text-white">λ‹«κΈ° λ²„νŠΌλ§Œ μžˆλŠ” λͺ¨λ‹¬μž…λ‹ˆλ‹€!</p>
</Modal>

</div>
);
}
125 changes: 125 additions & 0 deletions src/components/ui/modal/Modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { Button } from '@/components/ui/Button';
import React from 'react';

interface AlertModalProps {
isOpen: boolean;
onClose: () => void;
onConfirm?: () => void;
confirmText?: string;
cancelText?: string;
children: React.ReactNode;
modalClassName?: string;
contentClassName?: string;
buttonClassName?: string;
closeOnly?: boolean;
}
Comment on lines +4 to +15
Copy link

Choose a reason for hiding this comment

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

πŸ› οΈ Refactor suggestion

μ ‘κ·Όμ„± κ΄€λ ¨ ν”„λ‘œνΌν‹° μΆ”κ°€ ν•„μš”

λͺ¨λ‹¬μ˜ 접근성을 κ°œμ„ ν•˜κΈ° μœ„ν•΄ ν•„μˆ˜μ μΈ ARIA 속성듀을 ν”„λ‘œνΌν‹°λ‘œ μΆ”κ°€ν•΄μ•Ό ν•©λ‹ˆλ‹€.

λ‹€μŒκ³Ό 같이 μΈν„°νŽ˜μ΄μŠ€λ₯Ό ν™•μž₯ν•˜μ„Έμš”:

 interface AlertModalProps {
   isOpen: boolean;
   onClose: () => void;
   onConfirm?: () => void;
   confirmText?: string;
   cancelText?: string;
   children: React.ReactNode;
   modalClassName?: string;
   contentClassName?: string;
   buttonClassName?: string;
   closeOnly?: boolean;
+  'aria-labelledby'?: string;
+  'aria-describedby'?: string;
 }
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
interface AlertModalProps {
isOpen: boolean;
onClose: () => void;
onConfirm?: () => void;
confirmText?: string;
cancelText?: string;
children: React.ReactNode;
modalClassName?: string;
contentClassName?: string;
buttonClassName?: string;
closeOnly?: boolean;
}
interface AlertModalProps {
isOpen: boolean;
onClose: () => void;
onConfirm?: () => void;
confirmText?: string;
cancelText?: string;
children: React.ReactNode;
modalClassName?: string;
contentClassName?: string;
buttonClassName?: string;
closeOnly?: boolean;
'aria-labelledby'?: string;
'aria-describedby'?: string;
}


/**
* Modal μ»΄ν¬λ„ŒνŠΈλŠ” μ‚¬μš©μž μΈν„°νŽ˜μ΄μŠ€μ— λͺ¨λ‹¬ λ‹€μ΄μ–Όλ‘œκ·Έλ₯Ό ν‘œμ‹œν•˜λŠ” μž¬μ‚¬μš© κ°€λŠ₯ν•œ μ»΄ν¬λ„ŒνŠΈμž…λ‹ˆλ‹€.
*
* @component
* @example
* ```tsx
* // κΈ°λ³Έ λͺ¨λ‹¬ (확인/μ·¨μ†Œ λ²„νŠΌ)
* <Modal
* isOpen={true}
* onClose={() => setIsOpen(false)}
* onConfirm={() => handleConfirm()}
* confirmText="확인"
* cancelText="μ·¨μ†Œ"
* modalClassName="w-96"
* >
* <p>λͺ¨λ‹¬ λ‚΄μš©</p>
* </Modal>
*
* // λ‹«κΈ° λ²„νŠΌλ§Œ μžˆλŠ” λͺ¨λ‹¬
* <Modal
* isOpen={true}
* onClose={() => setIsOpen(false)}
* closeOnly
* cancelText="λ‹«κΈ°"
* modalClassName="w-96"
* >
* <p>λͺ¨λ‹¬ λ‚΄μš©</p>
* </Modal>
* ```
*
* @param props - λͺ¨λ‹¬ μ»΄ν¬λ„ŒνŠΈ ν”„λ‘œνΌν‹°
* @param props.isOpen - λͺ¨λ‹¬μ˜ ν‘œμ‹œ μ—¬λΆ€λ₯Ό μ œμ–΄
* @param props.onClose - λͺ¨λ‹¬μ΄ λ‹«νž λ•Œ ν˜ΈμΆœλ˜λŠ” 콜백 ν•¨μˆ˜
* @param props.onConfirm - 확인 λ²„νŠΌ 클릭 μ‹œ ν˜ΈμΆœλ˜λŠ” 콜백 ν•¨μˆ˜ (closeOnlyκ°€ false일 λ•Œλ§Œ ν•„μš”)
* @param props.closeOnly - true일 경우 λ‹«κΈ° λ²„νŠΌλ§Œ ν‘œμ‹œ (κΈ°λ³Έκ°’: false)
* @param props.confirmText - 확인 λ²„νŠΌμ˜ ν…μŠ€νŠΈ (κΈ°λ³Έκ°’: '확인', closeOnlyκ°€ false일 λ•Œλ§Œ μ‚¬μš©)
* @param props.cancelText - μ·¨μ†Œ/λ‹«κΈ° λ²„νŠΌμ˜ ν…μŠ€νŠΈ (κΈ°λ³Έκ°’: closeOnlyκ°€ true일 λ•Œ 'λ‹«κΈ°', false일 λ•Œ 'μ·¨μ†Œ')
* @param props.children - λͺ¨λ‹¬ 내뢀에 ν‘œμ‹œλ  컨텐츠
* @param props.modalClassName - λͺ¨λ‹¬ μ»¨ν…Œμ΄λ„ˆμ— μ μš©ν•  μΆ”κ°€ 클래슀λͺ…
* @param props.contentClassName - λͺ¨λ‹¬ 컨텐츠 μ˜μ—­μ— μ μš©ν•  μΆ”κ°€ 클래슀λͺ…
* @param props.buttonClassName - λ²„νŠΌ μ˜μ—­μ— μ μš©ν•  μΆ”κ°€ 클래슀λͺ…
*
* @returns React μ»΄ν¬λ„ŒνŠΈ
*/

const Modal: React.FC<AlertModalProps> = ({
isOpen,
onClose,
onConfirm,
confirmText = '확인',
cancelText = 'μ·¨μ†Œ',
children,
modalClassName = '',
contentClassName = '',
buttonClassName = '',
closeOnly = false,
}) => {
const handleBackdropClick = (e: React.MouseEvent<HTMLDivElement>): void => {
if (e.target === e.currentTarget) {
onClose();
}
};

const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>): void => {
if (e.key === 'Escape') {
onClose();
}
};
Comment on lines +74 to +84
Copy link

Choose a reason for hiding this comment

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

πŸ› οΈ Refactor suggestion

ν‚€λ³΄λ“œ μ ‘κ·Όμ„± κ°œμ„  ν•„μš”

λͺ¨λ‹¬μ΄ 열렸을 λ•Œ 포컀슀 관리와 ν‚€λ³΄λ“œ 탐색이 μ œλŒ€λ‘œ κ΅¬ν˜„λ˜μ–΄ μžˆμ§€ μ•ŠμŠ΅λ‹ˆλ‹€.

λ‹€μŒ 사항듀을 κ°œμ„ ν•˜μ„Έμš”:

  1. λͺ¨λ‹¬μ΄ 열릴 λ•Œ 첫 번째 포컀슀 κ°€λŠ₯ν•œ μš”μ†Œλ‘œ 포컀슀 이동
  2. Tab ν‚€λ‘œ λͺ¨λ‹¬ λ‚΄λΆ€ μš”μ†Œλ“€λ§Œ μˆœν™˜ν•˜λ„λ‘ 포컀슀 트랩 κ΅¬ν˜„
  3. λͺ¨λ‹¬μ΄ λ‹«νž λ•Œ 이전 포컀슀 μœ„μΉ˜λ‘œ 볡원
+import { useEffect, useRef } from 'react';
+
 const Modal: React.FC<AlertModalProps> = ({
   // ... props
 }) => {
+  const modalRef = useRef<HTMLDivElement>(null);
+  const previousFocusRef = useRef<HTMLElement | null>(null);
+
+  useEffect(() => {
+    if (isOpen) {
+      previousFocusRef.current = document.activeElement as HTMLElement;
+      const focusableElements = modalRef.current?.querySelectorAll(
+        'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
+      );
+      if (focusableElements?.[0]) {
+        (focusableElements[0] as HTMLElement).focus();
+      }
+    } else if (previousFocusRef.current) {
+      previousFocusRef.current.focus();
+    }
+  }, [isOpen]);
+
+  const handleTabKey = (e: React.KeyboardEvent) => {
+    if (e.key !== 'Tab') return;
+    
+    const focusableElements = modalRef.current?.querySelectorAll(
+      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
+    );
+    if (!focusableElements) return;
+    
+    const firstElement = focusableElements[0] as HTMLElement;
+    const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement;
+    
+    if (e.shiftKey && document.activeElement === firstElement) {
+      e.preventDefault();
+      lastElement.focus();
+    } else if (!e.shiftKey && document.activeElement === lastElement) {
+      e.preventDefault();
+      firstElement.focus();
+    }
+  };
+
   const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>): void => {
     if (e.key === 'Escape') {
       onClose();
     }
+    handleTabKey(e);
   };
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleBackdropClick = (e: React.MouseEvent<HTMLDivElement>): void => {
if (e.target === e.currentTarget) {
onClose();
}
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>): void => {
if (e.key === 'Escape') {
onClose();
}
};
import { useEffect, useRef } from 'react';
const Modal: React.FC<AlertModalProps> = ({
// ... props (including isOpen, onClose, etc.)
}) => {
const modalRef = useRef<HTMLDivElement>(null);
const previousFocusRef = useRef<HTMLElement | null>(null);
useEffect(() => {
if (isOpen) {
previousFocusRef.current = document.activeElement as HTMLElement;
const focusableElements = modalRef.current?.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
if (focusableElements?.[0]) {
(focusableElements[0] as HTMLElement).focus();
}
} else if (previousFocusRef.current) {
previousFocusRef.current.focus();
}
}, [isOpen]);
const handleTabKey = (e: React.KeyboardEvent) => {
if (e.key !== 'Tab') return;
const focusableElements = modalRef.current?.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
if (!focusableElements) return;
const firstElement = focusableElements[0] as HTMLElement;
const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement;
if (e.shiftKey && document.activeElement === firstElement) {
e.preventDefault();
lastElement.focus();
} else if (!e.shiftKey && document.activeElement === lastElement) {
e.preventDefault();
firstElement.focus();
}
};
const handleBackdropClick = (e: React.MouseEvent<HTMLDivElement>): void => {
if (e.target === e.currentTarget) {
onClose();
}
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>): void => {
if (e.key === 'Escape') {
onClose();
}
handleTabKey(e);
};
return (
<div
ref={modalRef}
onKeyDown={handleKeyDown}
// ... other props and JSX for the modal
>
{/* Modal content */}
</div>
);
};


if (!isOpen) return null;

return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-BG_2 bg-opacity-50"
onClick={handleBackdropClick}
onKeyDown={handleKeyDown}
role="presentation"
aria-label="Close modal"
>
<div
className={`rounded-lg bg-BG_2 shadow-xl ${modalClassName}`}
role="dialog"
aria-modal="true"
tabIndex={-1}
>
<div className={`p-6 ${contentClassName}`}>{children}</div>

<div className={`flex justify-end gap-2 p-4 ${buttonClassName}`}>
{closeOnly ? (
<Button onClick={onClose} type="button">
{cancelText}
</Button>
) : (
<>
<Button onClick={onClose} variant={'outline'} type="button">
{cancelText}
</Button>
<Button onClick={onConfirm} type="button">
{confirmText}
</Button>
</>
)}
</div>
</div>
</div>
);
Comment on lines +88 to +122
Copy link

Choose a reason for hiding this comment

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

πŸ› οΈ Refactor suggestion

μ‹œλ§¨ν‹± ꡬ쑰와 슀크린 리더 지원 κ°œμ„  ν•„μš”

λͺ¨λ‹¬μ˜ μ‹œλ§¨ν‹± ꡬ쑰와 슀크린 리더 μ‚¬μš©μžλ₯Ό μœ„ν•œ 지원이 λ―Έν‘ν•©λ‹ˆλ‹€.

λ‹€μŒκ³Ό 같이 κ°œμ„ ν•˜μ„Έμš”:

 return (
   <div
     className="fixed inset-0 z-50 flex items-center justify-center bg-BG_2 bg-opacity-50"
     onClick={handleBackdropClick}
     onKeyDown={handleKeyDown}
-    role="presentation"
-    aria-label="Close modal"
+    aria-hidden="true"
   >
     <div
+      ref={modalRef}
       className={`rounded-lg bg-BG_2 shadow-xl ${modalClassName}`}
       role="dialog"
       aria-modal="true"
+      aria-labelledby={props['aria-labelledby']}
+      aria-describedby={props['aria-describedby']}
       tabIndex={-1}
     >
       <div className={`p-6 ${contentClassName}`}>{children}</div>
       <div className={`flex justify-end gap-2 p-4 ${buttonClassName}`}>
         {closeOnly ? (
           <Button onClick={onClose} type="button">
             {cancelText}
           </Button>
         ) : (
           <>
             <Button onClick={onClose} variant={'outline'} type="button">
               {cancelText}
             </Button>
             <Button onClick={onConfirm} type="button">
               {confirmText}
             </Button>
           </>
         )}
       </div>
     </div>
   </div>
 );

};

export default Modal;
4 changes: 2 additions & 2 deletions tailwind.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export default {
lg: '745px',
},
colors: {
main: '#3853EA', // 이전 #525FEE
main: '#3853EA',
default: '#C2C9FF',
solid: '#E5e7fa',
disable: '#30333e',
Expand All @@ -32,7 +32,7 @@ export default {
Cgray700: '#B4BBCE',
Cgray800: '#D8DEE8',
BG: '#0F0F0F',
BG_2: '1B1B1D',
BG_2: '#1B1B1D',
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
card: {
Expand Down
Loading