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/alert/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;
42 changes: 42 additions & 0 deletions src/app/preview/modal/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
'use client';

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

export default function ModalTestPage() {
const [isModalOpen, setIsModalOpen] = useState(false);
const [isModalOpen2, setIsModalOpen2] = 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>
<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>

</div>
Copy link

@coderabbitai coderabbitai bot Feb 19, 2025

Choose a reason for hiding this comment

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

πŸ› οΈ Refactor suggestion

μ ‘κ·Όμ„± κ°œμ„  ν•„μš”

λͺ¨λ‹¬μ„ μ—¬λŠ” λ²„νŠΌλ“€μ— λŒ€ν•œ μ ‘κ·Όμ„± 속성이 λΆ€μ‘±ν•©λ‹ˆλ‹€.

λ²„νŠΌμ— μ μ ˆν•œ aria-labelκ³Ό aria-controls 속성을 μΆ”κ°€ν•˜μ—¬ 접근성을 κ°œμ„ ν•˜μ„Έμš”:

-<Button onClick={() => setIsModalOpen(true)}>이쀑 λͺ¨λ‹¬</Button>
-<Button onClick={() => setIsModalOpen2(true)}>κ·Έλƒ₯ λͺ¨λ‹¬</Button>
+<Button 
+  onClick={() => setIsModalOpen(true)}
+  aria-label="이쀑 λͺ¨λ‹¬ μ—΄κΈ°"
+  aria-controls="dual-modal"
+>
+  이쀑 λͺ¨λ‹¬
+</Button>
+<Button 
+  onClick={() => setIsModalOpen2(true)}
+  aria-label="κΈ°λ³Έ λͺ¨λ‹¬ μ—΄κΈ°"
+  aria-controls="simple-modal"
+>
+  κ·Έλƒ₯ λͺ¨λ‹¬
+</Button>

λ˜ν•œ 각 Modal μ»΄ν¬λ„ŒνŠΈμ— κ³ μœ ν•œ idλ₯Ό μΆ”κ°€ν•˜λŠ” 것이 μ’‹μŠ΅λ‹ˆλ‹€:

 <Modal
+  id="dual-modal"
   isOpen={isModalOpen}
   ...
 >

 <Modal
+  id="simple-modal"
   isOpen={isModalOpen2}
   ...
 >
πŸ“ 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
<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>
<Modal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
onConfirm={handleConfirm}
confirmText="μ‚­μ œ"
cancelText="μ·¨μ†Œ"
modalClassName="w-96"
>
<Some />
</Modal>
<Modal
isOpen={isModalOpen2}
onClose={() => setIsModalOpen2(false)}
onConfirm={handleConfirm}
confirmText="μ‚­μ œ"
cancelText="μ·¨μ†Œ"
modalClassName="w-96"
>
<p className="text-white">μ•ˆλ…•ν•˜μ„Έμš©μš©</p>
</Modal>
</div>
<div className="flex min-h-screen flex-col items-center justify-center gap-4 bg-BG p-4">
<Button
onClick={() => setIsModalOpen(true)}
aria-label="이쀑 λͺ¨λ‹¬ μ—΄κΈ°"
aria-controls="dual-modal"
>
이쀑 λͺ¨λ‹¬
</Button>
<Button
onClick={() => setIsModalOpen2(true)}
aria-label="κΈ°λ³Έ λͺ¨λ‹¬ μ—΄κΈ°"
aria-controls="simple-modal"
>
κ·Έλƒ₯ λͺ¨λ‹¬
</Button>
<Modal
id="dual-modal"
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
onConfirm={handleConfirm}
confirmText="μ‚­μ œ"
cancelText="μ·¨μ†Œ"
modalClassName="w-96"
>
<Some />
</Modal>
<Modal
id="simple-modal"
isOpen={isModalOpen2}
onClose={() => setIsModalOpen2(false)}
onConfirm={handleConfirm}
confirmText="μ‚­μ œ"
cancelText="μ·¨μ†Œ"
modalClassName="w-96"
>
<p className="text-white">μ•ˆλ…•ν•˜μ„Έμš©μš©</p>
</Modal>
</div>

Copy link
Contributor Author

Choose a reason for hiding this comment

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

aria속성과 접근성에 λŒ€ν•œμ •λ³΄, 그리고 Modal에 Idλ₯Ό μΆ”κ°€ν•΄μ•Όν•˜λŠ” 이유

Copy link

Choose a reason for hiding this comment

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

🧩 Analysis chain

@clyde-yoonjae ARIA(Accessible Rich Internet Applications) 속성과 접근성에 λŒ€ν•΄ μ„€λͺ…λ“œλ¦¬κ² μŠ΅λ‹ˆλ‹€:

  1. ARIA μ†μ„±μ΄λž€?

    • ARIAλŠ” 슀크린 리더와 같은 보쑰 기술이 μ›Ή μ½˜ν…μΈ λ₯Ό 더 잘 이해할 수 μžˆλ„λ‘ λ•λŠ” μΆ”κ°€ 정보λ₯Ό μ œκ³΅ν•©λ‹ˆλ‹€
    • aria-label: μš”μ†Œμ— λŒ€ν•œ μ„€λͺ…을 제곡 (예: "이쀑 λͺ¨λ‹¬ μ—΄κΈ°")
    • aria-controls: ν˜„μž¬ μš”μ†Œκ°€ μ œμ–΄ν•˜λŠ” λ‹€λ₯Έ μš”μ†Œμ™€μ˜ 관계λ₯Ό λͺ…μ‹œ
  2. λͺ¨λ‹¬ IDκ°€ ν•„μš”ν•œ 이유

    • aria-controlsκ°€ μ œλŒ€λ‘œ μž‘λ™ν•˜λ €λ©΄ μ œμ–΄ λŒ€μƒ μš”μ†Œ(λͺ¨λ‹¬)에 κ³ μœ ν•œ IDκ°€ ν•„μš”ν•©λ‹ˆλ‹€
    • 슀크린 λ¦¬λ”λŠ” 이 IDλ₯Ό 톡해 λ²„νŠΌκ³Ό λͺ¨λ‹¬μ˜ 관계λ₯Ό νŒŒμ•…ν•  수 μžˆμŠ΅λ‹ˆλ‹€
    • 예: λ²„νŠΌμ˜ aria-controls="dual-modal"은 IDκ°€ "dual-modal"인 λͺ¨λ‹¬μ„ μ œμ–΄ν•œλ‹€λŠ” 것을 λ‚˜νƒ€λƒ…λ‹ˆλ‹€
  3. μ ‘κ·Όμ„± κ°œμ„  효과

    • μ‹œκ° μž₯애인 μ‚¬μš©μžκ°€ μ–΄λ–€ λ²„νŠΌμ΄ μ–΄λ–€ λͺ¨λ‹¬μ„ μ œμ–΄ν•˜λŠ”μ§€ 이해할 수 μžˆμŠ΅λ‹ˆλ‹€
    • ν‚€λ³΄λ“œ μ‚¬μš©μžκ°€ λͺ¨λ‹¬κ³Ό κ΄€λ ¨ μ»¨νŠΈλ‘€μ„ 더 μ‰½κ²Œ 탐색할 수 μžˆμŠ΅λ‹ˆλ‹€
    • μ „λ°˜μ μΈ μ‚¬μš©μž κ²½ν—˜μ΄ ν–₯μƒλ©λ‹ˆλ‹€

(_/)

(^.^)

(> <) πŸ“š μƒˆλ‘œμš΄ 지식을 λ°°μ› μ–΄μš”!


🌐 Web query:

What are ARIA attributes and why are they important for accessibility?

πŸ’‘ Result:

ARIA (Accessible Rich Internet Applications) attributes are a set of HTML extensions developed by the World Wide Web Consortium (W3C) to enhance web accessibility for users with disabilities[1][4][6]. They provide supplemental semantic information about elements' roles, states, and properties to assistive technologies like screen readers[5][8].

Key Components of ARIA

Roles define an element's purpose (e.g., role="button" converts a <div> into a button for screen readers)[5][9].
Properties describe characteristics (aria-label, aria-describedby) that help establish relationships between elements[5][7].
States indicate dynamic conditions (aria-expanded="true", aria-checked="false") that update during user interactions[5][9].

Why ARIA Matters for Accessibility

  1. Bridges HTML gaps: Enables accessible implementation of complex widgets (menus, sliders) that lack native HTML equivalents[4][6].
  2. Enhances dynamic content: Makes live updates (e.g., chat messages) accessible through attributes like aria-live[3][6].
  3. Improves navigation: Landmark roles (navigation, main) help users quickly locate page sections[3][6].
  4. Supports WCAG compliance: Helps meet accessibility standards by:
    • Providing text alternatives via aria-label[3]
    • Indicating required fields with aria-required[3]
    • Managing focus states in modals (aria-modal="true")[9]

Best Practices

  • Use native HTML first: Prefer semantic elements (<button>) over ARIA-enhanced <div>s when possible[4][8].
  • Test thoroughly: 41% more errors occur on pages with poorly implemented ARIA compared to those without[4].
  • Layer progressively: Add roles/states only when necessary to avoid overwhelming users[2][5].

Example of proper ARIA usage:

<div role="checkbox" aria-checked="false" tabindex="0"> 
  Receive newsletter
</div>

This creates a checkbox component accessible via keyboard and screen readers[9].

ARIA's importance lies in its ability to make modern web applications perceivable and operable for users who rely on assistive technologies while maintaining compatibility with evolving web standards[1][6][8]. When implemented correctly alongside semantic HTML, it creates equitable digital experiences without compromising functionality[2][4].

Citations:


@clyde-yoonjae ARIA와 접근성에 λŒ€ν•΄ 더 μžμ„Ένžˆ μ„€λͺ…λ“œλ¦¬κ² μŠ΅λ‹ˆλ‹€:

  1. ARIA (Accessible Rich Internet Applications)

    • W3Cμ—μ„œ κ°œλ°œν•œ HTML ν™•μž₯ κ·œκ²©μž…λ‹ˆλ‹€
    • μ„Έ κ°€μ§€ μ£Όμš” ꡬ성 μš”μ†Œκ°€ μžˆμŠ΅λ‹ˆλ‹€:
      • Roles: μš”μ†Œμ˜ λͺ©μ  μ •μ˜ (예: role="dialog" for λͺ¨λ‹¬)
      • Properties: μš”μ†Œμ˜ νŠΉμ„± μ„€λͺ… (aria-label, aria-controls)
      • States: 동적 μƒνƒœ ν‘œμ‹œ (aria-expanded, aria-hidden)
  2. λͺ¨λ‹¬μ—μ„œ ARIA μ†μ„±μ˜ μ€‘μš”μ„±

    <!-- 쒋은 μ˜ˆμ‹œ -->
    <button aria-label="이쀑 λͺ¨λ‹¬ μ—΄κΈ°" aria-controls="dual-modal">
      이쀑 λͺ¨λ‹¬
    </button>
    <div 
      id="dual-modal" 
      role="dialog" 
      aria-modal="true"
      aria-labelledby="modal-title"
    >
      <!-- λͺ¨λ‹¬ λ‚΄μš© -->
    </div>
  3. IDκ°€ ν•„μš”ν•œ 이유

    • aria-controls와 aria-labelledby 같은 ARIA 관계 속성듀이 μ œλŒ€λ‘œ μž‘λ™ν•˜λ €λ©΄ λŒ€μƒ μš”μ†Œμ˜ IDκ°€ ν•„μš”ν•©λ‹ˆλ‹€
    • 슀크린 리더가 이 관계λ₯Ό μΈμ‹ν•˜κ³  μ‚¬μš©μžμ—κ²Œ μ μ ˆν•œ μ»¨ν…μŠ€νŠΈλ₯Ό μ œκ³΅ν•  수 μžˆμŠ΅λ‹ˆλ‹€
  4. λͺ¨λ²” 사둀

    • κ°€λŠ₯ν•œ ν•œ μ‹œλ§¨ν‹± HTML μš”μ†Œλ₯Ό λ¨Όμ € μ‚¬μš©ν•˜μ„Έμš”
    • ARIAλŠ” HTML만으둜 μΆ©λΆ„ν•˜μ§€ μ•Šμ„ λ•Œ λ³΄μ™„μ μœΌλ‘œ μ‚¬μš©ν•©λ‹ˆλ‹€
    • λͺ¨λ‹¬ κ΅¬ν˜„ μ‹œ role="dialog"와 aria-modal="true"λ₯Ό ν•¨κ»˜ μ‚¬μš©ν•˜λ©΄ μ’‹μŠ΅λ‹ˆλ‹€

(_/)

(^.^)

(> <) πŸ“š μ ‘κ·Όμ„±μ˜ μ„Έκ³„λŠ” 정말 κΉŠλ„€μš”!


✏️ Learnings added
Learnt from: clyde-yoonjae
PR: MoimService/Moim-FE#14
File: src/app/preview/modal/page.tsx:17-40
Timestamp: 2025-02-19T02:34:10.245Z
Learning: ARIA 속성은 μ›Ή 접근성을 ν–₯μƒμ‹œν‚€λŠ” μ€‘μš”ν•œ λ„κ΅¬μž…λ‹ˆλ‹€:
- aria-label: μš”μ†Œμ— λŒ€ν•œ μ„€λͺ…적인 λ ˆμ΄λΈ”μ„ 제곡
- aria-controls: ν˜„μž¬ μš”μ†Œκ°€ μ œμ–΄ν•˜λŠ” λ‹€λ₯Έ μš”μ†Œμ™€μ˜ 관계λ₯Ό λͺ…μ‹œ
λͺ¨λ‹¬ μ»΄ν¬λ„ŒνŠΈμ—λŠ” 항상 κ³ μœ ν•œ IDλ₯Ό λΆ€μ—¬ν•˜κ³ , ν•΄λ‹Ή λͺ¨λ‹¬μ„ μ œμ–΄ν•˜λŠ” λ²„νŠΌμ—λŠ” aria-controls μ†μ„±μœΌλ‘œ λͺ¨λ‹¬ IDλ₯Ό μ—°κ²°ν•΄μ•Ό ν•©λ‹ˆλ‹€.

Learnt from: clyde-yoonjae
PR: MoimService/Moim-FE#14
File: src/app/preview/modal/page.tsx:17-40
Timestamp: 2025-02-19T02:34:10.245Z
Learning: ARIA 속성은 μ„Έ κ°€μ§€ μ£Όμš” μΉ΄ν…Œκ³ λ¦¬λ‘œ κ΅¬λΆ„λ©λ‹ˆλ‹€:
1. Roles: μš”μ†Œμ˜ λͺ©μ  μ •μ˜ (예: role="dialog")
2. Properties: μš”μ†Œμ˜ νŠΉμ„± μ„€λͺ… (예: aria-label, aria-controls)
3. States: 동적 μƒνƒœ ν‘œμ‹œ (예: aria-expanded, aria-hidden)

λͺ¨λ‹¬ κ΅¬ν˜„ μ‹œ ν•„μˆ˜ ARIA 속성:
- λͺ¨λ‹¬ 트리거 λ²„νŠΌ: aria-label, aria-controls
- λͺ¨λ‹¬ μ»¨ν…Œμ΄λ„ˆ: role="dialog", aria-modal="true", aria-labelledby
λͺ¨λ‹¬κ³Ό κ΄€λ ¨ μš”μ†Œλ“€μ€ λ°˜λ“œμ‹œ κ³ μœ ν•œ IDλ₯Ό κ°€μ Έμ•Ό ν•˜λ©°, μ΄λŠ” ARIA 관계 속성듀이 μ˜¬λ°”λ₯΄κ²Œ μž‘λ™ν•˜κΈ° μœ„ν•΄ ν•„μš”ν•©λ‹ˆλ‹€.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

);
}
105 changes: 105 additions & 0 deletions src/components/ui/modal/alert/Modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
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;
}

/**
* Modal μ»΄ν¬λ„ŒνŠΈλŠ” μ‚¬μš©μž μΈν„°νŽ˜μ΄μŠ€μ— λͺ¨λ‹¬ λ‹€μ΄μ–Όλ‘œκ·Έλ₯Ό ν‘œμ‹œν•˜λŠ” μž¬μ‚¬μš© κ°€λŠ₯ν•œ μ»΄ν¬λ„ŒνŠΈμž…λ‹ˆλ‹€.
*
* @component
* @example
* ```tsx
* <Modal
* isOpen={true}
* onClose={() => setIsOpen(false)}
* onConfirm={() => handleConfirm()}
* confirmText="확인"
* cancelText="μ·¨μ†Œ"
* modalClassName="w-96"
* >
* <p>λͺ¨λ‹¬ λ‚΄μš©</p>
* </Modal>
* ```
*
* @param props - λͺ¨λ‹¬ μ»΄ν¬λ„ŒνŠΈ ν”„λ‘œνΌν‹°
* @param props.isOpen - λͺ¨λ‹¬μ˜ ν‘œμ‹œ μ—¬λΆ€λ₯Ό μ œμ–΄
* @param props.onClose - λͺ¨λ‹¬μ΄ λ‹«νž λ•Œ ν˜ΈμΆœλ˜λŠ” 콜백 ν•¨μˆ˜
* @param props.onConfirm - 확인 λ²„νŠΌ 클릭 μ‹œ ν˜ΈμΆœλ˜λŠ” 콜백 ν•¨μˆ˜
* @param props.confirmText - 확인 λ²„νŠΌμ˜ ν…μŠ€νŠΈ (κΈ°λ³Έκ°’: '확인')
* @param props.cancelText - μ·¨μ†Œ λ²„νŠΌμ˜ ν…μŠ€νŠΈ (κΈ°λ³Έκ°’: 'μ·¨μ†Œ')
* @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 = '',
}) => {
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();
}
};

if (!isOpen) return null;

return (
// backdrop
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-BG_2 bg-opacity-50"
onClick={handleBackdropClick}
onKeyDown={handleKeyDown}
role="button"
tabIndex={0}
aria-label="Close modal"
>
Copy link

@coderabbitai coderabbitai bot Feb 19, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

backdrop의 μ ‘κ·Όμ„± 및 이벀트 처리 κ°œμ„  ν•„μš”

backdrop div에 role="button"을 μ‚¬μš©ν•˜λŠ” 것은 μ μ ˆν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. λ˜ν•œ ν‚€λ³΄λ“œ 이벀트 μ²˜λ¦¬κ°€ λΆˆμ™„μ „ν•©λ‹ˆλ‹€.

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

 <div
   className="fixed inset-0 z-50 flex items-center justify-center bg-BG_2 bg-opacity-50"
   onClick={handleBackdropClick}
   onKeyDown={handleKeyDown}
-  role="button"
+  role="presentation"
   tabIndex={0}
-  aria-label="Close 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
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-BG_2 bg-opacity-50"
onClick={handleBackdropClick}
onKeyDown={handleKeyDown}
role="button"
tabIndex={0}
aria-label="Close modal"
>
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-BG_2 bg-opacity-50"
onClick={handleBackdropClick}
onKeyDown={handleKeyDown}
role="presentation"
tabIndex={0}
>

Copy link
Contributor Author

Choose a reason for hiding this comment

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

tabIndexλŠ” μƒν˜Έμž‘μš© κ°€λŠ₯ν•œ μš”μ†ŒμΈ λ²„νŠΌμ΄λ‚˜ λ§ν¬μ—λ§Œ 달 수 μžˆλŠ”λ° μ΄λ ‡κ²Œ μˆ˜μ •ν•˜λŠ”κ²Œ λ§žμ„κΉŒ?

Copy link

Choose a reason for hiding this comment

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

🧩 Analysis chain

@clyde-yoonjae λ„€, 쒋은 μ§€μ μž…λ‹ˆλ‹€! backdrop divμ—λŠ” tabIndexκ°€ ν•„μš”ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. λŒ€μ‹  λ‹€μŒκ³Ό 같이 μˆ˜μ •ν•˜λŠ” 것이 μ’‹μŠ΅λ‹ˆλ‹€:

  1. backdrop divμ—μ„œ tabIndex와 role="button"을 제거
  2. μ‹€μ œ modal container에 μ μ ˆν•œ ARIA 속성 μΆ”κ°€
// backdrop
<div
  className="fixed inset-0 z-50 flex items-center justify-center bg-BG_2 bg-opacity-50"
  onClick={handleBackdropClick}
  role="presentation"
>
  {/* modal container */}
  <div
    role="dialog"
    aria-modal="true"
    className={`relative bg-white rounded-lg ${modalClassName}`}
  >
    {children}
  </div>
</div>

μ΄λ ‡κ²Œ ν•˜λ©΄ 슀크린 리더 μ‚¬μš©μžλ“€μ—κ²Œ 더 λͺ…ν™•ν•œ 의미λ₯Ό 전달할 수 μžˆμŠ΅λ‹ˆλ‹€.


🌐 Web query:

ARIA modal dialog best practices for backdrop and focus management

πŸ’‘ Result:

To create accessible ARIA modal dialogs with proper backdrop handling and focus management, follow these key practices:

Backdrop Implementation

  • Use nested <div> elements when working with native <dialog> to independently control modal window and backdrop styling/behavior[2][10]
  • Prevent background interaction by:
    • Applying aria-hidden="true" to non-dialog content
    • Setting tabindex="-1" on interactive background elements[1][4]
    • Disabling scroll via CSS (overflow: hidden on body)[2][10]
<div class="backdrop" aria-hidden="true">
  <div role="dialog" aria-modal="true">
    <!-- Dialog content -->
  </div>
</div>

Focus Management

  • Trap keyboard focus using JavaScript to:

    • Cycle TAB within modal boundaries[5][7]
    • Close on ESC key press[3][10]
  • Initial focus placement:

    • First interactive element (default for <dialog>)[3]
    • Static content container (tabindex="-1") when needing introductory context[2][7]
// Focus trapping example
document.addEventListener('keydown', (e) => {
  if (e.key === 'Tab' && modalOpen) {
    // Cycle focus within modal
  }
});

ARIA Attributes Configuration

Attribute Purpose Example
role="dialog" Identifies dialog container <div role="dialog">
aria-modal="true" Declares modal nature Mandatory for all modals[4]
aria-labelledby Associates dialog title Points to heading ID[10]
aria-describedby Optional content description Points to content ID[7]

Visual/Interaction Requirements

  • Backdrop styling should visually distinguish modal content:
.dialog-backdrop {
  position: fixed;
  background: rgba(0,0,0,0.5);
  inset: 0;
}
  • Close functionality must include:
    • Visible close button (X icon)
    • ESC key support[3][5]
    • Backdrop click (optional)[2]

When implementing modals, always test with screen readers like NVDA/VoiceOver and follow WAI-ARIA Authoring Practices[7]. Native <dialog> element usage is recommended when possible due to built-in browser accessibility features[3][9], but custom implementations require careful attention to focus trapping and ARIA semantics[1][4].

Citations:


@clyde-yoonjae μ£„μ†‘ν•©λ‹ˆλ‹€. 이전 μ œμ•ˆμ΄ λΆˆμ™„μ „ν–ˆλ„€μš”. ARIA 베슀트 ν”„λž™ν‹°μŠ€μ— λ”°λ₯΄λ©΄, λͺ¨λ‹¬ κ΅¬ν˜„μ‹œ λ‹€μŒκ³Ό 같은 νŒ¨ν„΄μ„ μ‚¬μš©ν•΄μ•Ό ν•©λ‹ˆλ‹€:

const Modal: React.FC<AlertModalProps> = ({
  isOpen,
  onClose,
  onConfirm,
  confirmText = '확인',
  cancelText = 'μ·¨μ†Œ',
  children,
  modalClassName = '',
  contentClassName = '',
  buttonClassName = '',
}) => {
  // ... existing code ...

  return (
    // backdrop
    <div
      className="fixed inset-0 z-50 flex items-center justify-center bg-BG_2 bg-opacity-50"
      onClick={handleBackdropClick}
      aria-hidden="true"
    >
      {/* modal container */}
      <div
        role="dialog"
        aria-modal="true"
        aria-labelledby="modal-title"
        className={`relative bg-white rounded-lg ${modalClassName}`}
      >
        <h2 id="modal-title" className="sr-only">λͺ¨λ‹¬ λ‹€μ΄μ–Όλ‘œκ·Έ</h2>
        <div className={contentClassName}>
          {children}
        </div>
        <div className={buttonClassName}>
          <Button onClick={onConfirm}>{confirmText}</Button>
          <Button onClick={onClose}>{cancelText}</Button>
        </div>
      </div>
    </div>
  );
};

μ£Όμš” 변경사항:

  1. backdrop divμ—μ„œ tabIndex와 role μ œκ±°ν•˜κ³  λŒ€μ‹  aria-hidden="true" μΆ”κ°€
  2. λͺ¨λ‹¬ μ»¨ν…Œμ΄λ„ˆμ— μ μ ˆν•œ ARIA 속성듀 μΆ”κ°€ (role="dialog", aria-modal="true", aria-labelledby)
  3. 슀크린 리더λ₯Ό μœ„ν•œ 제λͺ© μΆ”κ°€

μΆ”κ°€λ‘œ 포컀슀 관리λ₯Ό μœ„ν•œ λ‘œμ§λ„ ν•„μš”ν•˜μ§€λ§Œ, 그건 λ³„λ„μ˜ PRμ—μ„œ λ‹€λ£¨λŠ” 것이 쒋을 것 κ°™μŠ΅λ‹ˆλ‹€.


✏️ Learnings added
Learnt from: clyde-yoonjae
PR: MoimService/Moim-FE#14
File: src/components/ui/modal/alert/Modal.tsx:75-82
Timestamp: 2025-02-19T04:05:58.610Z
Learning: Modal backdrop divλŠ” `tabIndex`λ‚˜ `role="button"`을 μ‚¬μš©ν•˜μ§€ μ•Šμ•„μ•Ό ν•˜λ©°, λŒ€μ‹  `role="presentation"`을 μ‚¬μš©ν•©λ‹ˆλ‹€. μ‹€μ œ modal container에 `role="dialog"` 및 `aria-modal="true"`λ₯Ό μ μš©ν•˜μ—¬ 접근성을 보μž₯ν•΄μ•Ό ν•©λ‹ˆλ‹€.

Learnt from: clyde-yoonjae
PR: MoimService/Moim-FE#14
File: src/components/ui/modal/alert/Modal.tsx:75-82
Timestamp: 2025-02-19T04:05:58.610Z
Learning: λͺ¨λ‹¬ κ΅¬ν˜„μ‹œ 접근성을 μœ„ν•œ ARIA 속성 κ°€μ΄λ“œ:
1. Backdrop divλŠ” `aria-hidden="true"`λ₯Ό μ‚¬μš©ν•˜κ³  `tabIndex`λ‚˜ `role` 속성을 μ‚¬μš©ν•˜μ§€ μ•ŠμŒ
2. λͺ¨λ‹¬ μ»¨ν…Œμ΄λ„ˆλŠ” `role="dialog"`, `aria-modal="true"`, `aria-labelledby` 속성을 포함해야 함
3. 슀크린 리더λ₯Ό μœ„ν•œ 제λͺ©μ„ μ œκ³΅ν•˜κ³  `aria-labelledby`둜 μ—°κ²°ν•΄μ•Ό 함

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

{/* μ‹€μ œ λͺ¨λ‹¬ μ»¨ν…Œμ΄λ„ˆ */}
<div
className={`rounded-lg bg-BG_2 shadow-xl ${modalClassName}`}
role="dialog"
aria-modal="true"
tabIndex={-1}
>
Copy link

Choose a reason for hiding this comment

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

πŸ› οΈ Refactor suggestion

λͺ¨λ‹¬ λ‹€μ΄μ–Όλ‘œκ·Έμ˜ 포컀슀 관리 κ°œμ„  ν•„μš”

λͺ¨λ‹¬μ΄ 열릴 λ•Œ μžλ™μœΌλ‘œ 포컀슀λ₯Ό λ°›κ³ , λ‹«νž λ•Œ 이전 포컀슀둜 λŒμ•„κ°€λŠ” κΈ°λŠ₯이 ν•„μš”ν•©λ‹ˆλ‹€.

useEffectλ₯Ό μ‚¬μš©ν•˜μ—¬ 포컀슀 관리λ₯Ό κ΅¬ν˜„ν•˜μ„Έμš”:

+import { useEffect, useRef } from 'react';

 const Modal: React.FC<AlertModalProps> = ({...}) => {
+  const dialogRef = useRef<HTMLDivElement>(null);
+  const previousFocusRef = useRef<HTMLElement | null>(null);
+
+  useEffect(() => {
+    if (isOpen) {
+      previousFocusRef.current = document.activeElement as HTMLElement;
+      dialogRef.current?.focus();
+    } else if (previousFocusRef.current) {
+      previousFocusRef.current.focus();
+    }
+  }, [isOpen]);

   return (
     <div role="presentation">
       <div
+        ref={dialogRef}
         className={`rounded-lg bg-BG_2 shadow-xl ${modalClassName}`}
         role="dialog"
         aria-modal="true"
-        tabIndex={-1}
+        tabIndex={0}
       >
πŸ“ 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
<div
className={`rounded-lg bg-BG_2 shadow-xl ${modalClassName}`}
role="dialog"
aria-modal="true"
tabIndex={-1}
>
import React, { useEffect, useRef } from 'react';
interface AlertModalProps {
isOpen: boolean;
modalClassName?: string;
children?: React.ReactNode;
// ...other props
}
const Modal: React.FC<AlertModalProps> = ({ isOpen, modalClassName = '', children, ...props }) => {
const dialogRef = useRef<HTMLDivElement>(null);
const previousFocusRef = useRef<HTMLElement | null>(null);
useEffect(() => {
if (isOpen) {
previousFocusRef.current = document.activeElement as HTMLElement;
dialogRef.current?.focus();
} else if (previousFocusRef.current) {
previousFocusRef.current.focus();
}
}, [isOpen]);
return (
<div role="presentation">
<div
ref={dialogRef}
className={`rounded-lg bg-BG_2 shadow-xl ${modalClassName}`}
role="dialog"
aria-modal="true"
tabIndex={0}
{...props}
>
{children}
</div>
</div>
);
};
export default Modal;

<div className={`p-6 ${contentClassName}`}>{children}</div>

<div className={`flex justify-end gap-2 p-4 ${buttonClassName}`}>
<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