Skip to content

Conversation

@clyde-yoonjae
Copy link
Contributor

@clyde-yoonjae clyde-yoonjae commented Feb 18, 2025

πŸ“Β μ£Όμš” μž‘μ—… λ‚΄μš©

ν”„λ‘œμ νŠΈ μ „μ—­μ—μ„œ μ‚¬μš©ν•  λͺ¨λ‹¬μ»΄ν¬λ„ŒνŠΈ 직접 κ΅¬ν˜„ν•˜μ˜€μŠ΅λ‹ˆλ‹€.
/prview/modal
ν•΄λ‹Ήκ²½λ‘œμ— 일반 μ˜ˆμ‹œμ™€ mock 데이터 넣어놓은 2쀑λͺ¨λ‹¬ μ˜¬λ €λ‘μ—ˆμŠ΅λ‹ˆλ‹€.

πŸ“ΊΒ μŠ€ν¬λ¦°μƒ·

image
image

πŸ”— μ°Έκ³  사항

  • tailwind-config에 BG_2색상에 # 이 λΉ μ Έμžˆμ–΄ μˆ˜μ •ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

  • divνƒœκ·Έμ— μ†μ„±κ΄€λ ¨ν•˜μ—¬ 배움이 μžˆμ—ˆμŠ΅λ‹ˆλ‹€.

div μš”μ†Œμ˜ 이벀트 λ¦¬μŠ€λ„ˆ 직접 μΆ”κ°€ μ‹œ λ°œμƒν•˜λŠ” 문제점

1. μ ‘κ·Όμ„± 문제

  • 슀크린 리더가 이 μš”μ†Œλ₯Ό μƒν˜Έμž‘μš© κ°€λŠ₯ν•œ μš”μ†Œλ‘œ μΈμ‹ν•˜μ§€ λͺ»ν•  수 있음
  • ν‚€λ³΄λ“œ μ‚¬μš©μžκ°€ 이 μš”μ†Œμ™€ μ œλŒ€λ‘œ μƒν˜Έμž‘μš©ν•˜κΈ° 어렀움
  • 보쑰 기술이 이 μš”μ†Œμ˜ μš©λ„μ™€ κΈ°λŠ₯을 μ˜¬λ°”λ₯΄κ²Œ ν•΄μ„ν•˜μ§€ λͺ»ν•¨

2. 의미둠적(Semantic) 문제

  • divλŠ” 컨텐츠λ₯Ό κ΅¬λΆ„ν•˜λŠ” μš©λ„λ‘œ μ„€κ³„λ˜μ—ˆμ§€, μ‚¬μš©μž μƒν˜Έμž‘μš©μ„ μ²˜λ¦¬ν•˜λ„λ‘ μ„€κ³„λ˜μ§€ μ•ŠμŒ
  • HTML의 의미둠적 ꡬ쑰λ₯Ό ν•΄μΉ˜κ²Œ 됨

3. UX(μ‚¬μš©μž κ²½ν—˜) 문제

  • ν‚€λ³΄λ“œ 탐색 μ‹œ μ˜ˆμƒμΉ˜ λͺ»ν•œ λ™μž‘μ΄ λ°œμƒν•  수 있음
  • 포컀슀 관리가 μ œλŒ€λ‘œ 이루어지지 μ•Šμ„ 수 있음

ν•΄κ²° λ°©μ•ˆ

μƒν˜Έμž‘μš©μ΄ ν•„μš”ν•œ κ²½μš°μ—λŠ” λ‹€μŒ 두 κ°€μ§€ 방법 쀑 ν•˜λ‚˜λ₯Ό 선택해야 ν•©λ‹ˆλ‹€:

  1. buttonκ³Ό 같은 μƒν˜Έμž‘μš©μš© μš”μ†Œλ₯Ό μ‚¬μš©
  2. μ μ ˆν•œ roleκ³Ό tabIndexλ₯Ό μΆ”κ°€ν•˜μ—¬ ν•΄λ‹Ή μš”μ†Œμ˜ μƒν˜Έμž‘μš© λͺ©μ μ„ λͺ…ν™•νžˆ μ§€μ •

πŸ’¬Β λ¦¬λ·° μš”κ΅¬μ‚¬ν•­

ex) μ€‘μ μ μœΌλ‘œ λ¦¬λ·°ν•΄μ€¬μœΌλ©΄ ν•˜λŠ” λΆ€λΆ„

πŸ“ƒΒ κ΄€λ ¨ 이슈

DEVING-38

Summary by CodeRabbit

  • μƒˆλ‘œμš΄ κΈ°λŠ₯

    • μ‚¬μš©μž ν”„λ‘œν•„ 정보와 μƒνƒœλ³„ 색상 ν‘œμ‹œλ₯Ό μ œκ³΅ν•˜λŠ” λŒ€ν™”ν˜• λͺ¨λ‹¬ μΈν„°νŽ˜μ΄μŠ€κ°€ μΆ”κ°€λ˜μ—ˆμŠ΅λ‹ˆλ‹€.
    • μ—¬λŸ¬ μ’…λ₯˜μ˜ λͺ¨λ‹¬ 창을 ν…ŒμŠ€νŠΈν•  수 μžˆλŠ” νŽ˜μ΄μ§€κ°€ λ„μž…λ˜μ–΄, λ‹€μ–‘ν•œ νŒμ—… λ™μž‘μ„ μ²΄ν—˜ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
  • μŠ€νƒ€μΌ

    • UI 색상 ν‘œν˜„μ΄ κ°œμ„ λ˜μ–΄ 보닀 μΌκ΄€λœ λ””μžμΈ κ²½ν—˜μ„ μ œκ³΅ν•©λ‹ˆλ‹€.

@coderabbitai
Copy link

coderabbitai bot commented Feb 18, 2025

Walkthrough

이 ν’€ λ¦¬ν€˜μŠ€νŠΈλŠ” μƒˆλ‘œμš΄ λͺ¨λ‹¬ κ΄€λ ¨ UI 및 νŽ˜μ΄μ§€ μ»΄ν¬λ„ŒνŠΈλ₯Ό μΆ”κ°€ν•©λ‹ˆλ‹€. Some μ»΄ν¬λ„ŒνŠΈλŠ” μ‚¬μš©μž 데이터λ₯Ό ν‘œμ‹œν•˜κ³  λͺ¨λ‹¬μ„ 톡해 ν”„λ‘œν•„ 정보λ₯Ό 보여주며, ModalTestPageλŠ” μ„Έ 개의 λͺ¨λ‹¬ λ‹€μ΄μ–Όλ‘œκ·Έλ₯Ό μ‹œμ—°ν•©λ‹ˆλ‹€. λ˜ν•œ, μƒˆλ‘œμš΄ Modal μ»΄ν¬λ„ŒνŠΈκ°€ λ„μž…λ˜μ–΄ λ°±λ“œλ‘­ 클릭 및 ν‚€λ³΄λ“œ 이벀트λ₯Ό μ²˜λ¦¬ν•˜λ©°, Tailwind CSS μ„€μ •μ—μ„œλŠ” 색상 κ°’ ν˜•μ‹μ„ μˆ˜μ •ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

Changes

파일 λ³€κ²½ μš”μ•½
src/app/preview/modal/Some.tsx, src/app/preview/modal/page.tsx μƒˆλ‘œμš΄ React μ»΄ν¬λ„ŒνŠΈ μΆ”κ°€: Some은 μ‚¬μš©μž ν”„λ‘œν•„ ν‘œμ‹œ, ModalTestPageλŠ” 3개 λͺ¨λ‹¬ μ‹œμ—° (λ²„νŠΌ 클릭 μ‹œ λͺ¨λ‹¬ μ˜€ν”ˆ)
tailwind.config.ts 색상 μ •μ˜ μˆ˜μ •: BG_2 값에 '#' μΆ”κ°€ν•˜μ—¬ μ˜¬λ°”λ₯Έ ν—₯슀 μ½”λ“œ μ‚¬μš©
src/.../modal/Modal.tsx μƒˆλ‘œμš΄ Modal μ»΄ν¬λ„ŒνŠΈ μΆ”κ°€: AlertModalProps μΈν„°νŽ˜μ΄μŠ€ μ •μ˜, λ°±λ“œλ‘­ 및 ν‚€λ³΄λ“œ 이벀트 처리 포함

Sequence Diagram(s)

sequenceDiagram
    participant U as μ‚¬μš©μž
    participant S as Some μ»΄ν¬λ„ŒνŠΈ
    participant M as Modal
    U->>S: "ν”„λ‘œν•„ 보기" λ²„νŠΌ 클릭
    S->>S: handleProfileClick() μ‹€ν–‰
    S->>M: λͺ¨λ‹¬ μ—΄κΈ° (μ„ νƒλœ μ‚¬μš©μž 데이터 전달)
    U->>M: "확인" λ²„νŠΌ 클릭
    M->>S: handleSecondModalConfirm() μ‹€ν–‰
    S->>M: λͺ¨λ‹¬ λ‹«κΈ°
Loading
sequenceDiagram
    participant U as μ‚¬μš©μž
    participant P as ModalTestPage
    participant M1 as 첫 번째 λͺ¨λ‹¬ (Some 포함)
    participant M2 as 두 번째 λͺ¨λ‹¬ (ν…μŠ€νŠΈ λ©”μ‹œμ§€)
    participant M3 as μ„Έ 번째 λͺ¨λ‹¬ (λ‹«κΈ° μ „μš©)
    
    U->>P: 첫 번째 λ²„νŠΌ 클릭
    P->>M1: 첫 번째 λͺ¨λ‹¬ μ—΄κΈ°
    U->>M1: 확인/μ·¨μ†Œ λ™μž‘
    M1->>P: 첫 번째 λͺ¨λ‹¬ λ‹«κΈ°
    
    U->>P: 두 번째 λ²„νŠΌ 클릭
    P->>M2: 두 번째 λͺ¨λ‹¬ μ—΄κΈ°
    U->>M2: μ·¨μ†Œ λ™μž‘
    M2->>P: 두 번째 λͺ¨λ‹¬ λ‹«κΈ°
    
    U->>P: μ„Έ 번째 λ²„νŠΌ 클릭
    P->>M3: μ„Έ 번째 λͺ¨λ‹¬ μ—΄κΈ°
    U->>M3: λ‹«κΈ° λ™μž‘
    M3->>P: μ„Έ 번째 λͺ¨λ‹¬ λ‹«κΈ°
Loading

Suggested labels

πŸ”§ chore

Suggested reviewers

  • lee1nna
  • Lee-Dong-Seok

Poem

λ‚˜λŠ” κ·€μ—¬μš΄ 토끼, μ½”λ“œ μˆ²μ„ λ›°μ–΄λ‹€λ…€μš”,
μž‘μ€ λͺ¨λ‹¬ ν•˜λ‚˜μ— κΏˆμ„ λ‹΄μ•„, λˆˆλΆ€μ‹  빛을 λ³΄μ•„μš”.
λ²„νŠΌμ„ λˆ„λ₯΄λ©΄ 세상이 열리고,
μΈν„°νŽ˜μ΄μŠ€ 속 이야기가 좀을 μΆ°μš”.
ν–‰λ³΅ν•œ μ½”λ“œμ˜ λ…Έλž˜μ™€ ν•¨κ»˜ λͺ¨λ‘ μ¦κ±°μ›Œμ§€κΈΈ! 🐰🌟


πŸͺ§ Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@vercel
Copy link

vercel bot commented Feb 18, 2025

The latest updates on your projects. Learn more about Vercel for Git β†—οΈŽ

Name Status Preview Comments Updated (UTC)
moim-fe βœ… Ready (Inspect) Visit Preview πŸ’¬ Add feedback Feb 19, 2025 4:21am
moim-fe-1kc6 βœ… Ready (Inspect) Visit Preview πŸ’¬ Add feedback Feb 19, 2025 4:21am

@codecov
Copy link

codecov bot commented Feb 18, 2025

Codecov Report

All modified and coverable lines are covered by tests βœ…

@github-actions
Copy link

CI Status Report

검사 κ²°κ³Ό

  • Lint: βœ… success
  • Format: βœ… success
  • Type Check: βœ… success
  • Tests: βœ… success
  • Build: βœ… success

βœ… λͺ¨λ“  검사가 ν†΅κ³Όλ˜μ—ˆμŠ΅λ‹ˆλ‹€.

@clyde-yoonjae clyde-yoonjae self-assigned this Feb 18, 2025
@vercel vercel bot temporarily deployed to Preview – moim-fe February 19, 2025 02:25 Inactive
@github-actions
Copy link

CI Status Report

검사 κ²°κ³Ό

  • Lint: βœ… success
  • Format: βœ… success
  • Type Check: βœ… success
  • Tests: βœ… success
  • Build: βœ… success

βœ… λͺ¨λ“  검사가 ν†΅κ³Όλ˜μ—ˆμŠ΅λ‹ˆλ‹€.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (2)
src/app/preview/modal/page.tsx (1)

8-14: μƒνƒœ 관리 μ΅œμ ν™” μ œμ•ˆ

handleConfirm ν•¨μˆ˜κ°€ 첫 번째 λͺ¨λ‹¬μ—μ„œλ§Œ μ‚¬μš©λ˜κ³  있으며, 두 번째 λͺ¨λ‹¬μ—μ„œλŠ” λ™μΌν•œ ν•¨μˆ˜λ₯Ό μ‚¬μš©ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€. μ΄λŠ” 잠재적으둜 ν˜Όλž€μ„ μ•ΌκΈ°ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

각 λͺ¨λ‹¬μ— λŒ€ν•΄ λ³„λ„μ˜ handleConfirm ν•¨μˆ˜λ₯Ό λ§Œλ“€κ±°λ‚˜, λͺ¨λ‹¬ IDλ₯Ό λ§€κ°œλ³€μˆ˜λ‘œ λ°›λŠ” 단일 ν•¨μˆ˜λ₯Ό μ‚¬μš©ν•˜λŠ” 것을 κ³ λ €ν•΄λ³΄μ„Έμš”:

-const handleConfirm = () => {
-  setIsModalOpen(false);
-};
+const handleModalConfirm = (modalId: number) => {
+  if (modalId === 1) {
+    setIsModalOpen(false);
+  } else {
+    setIsModalOpen2(false);
+  }
+};
src/components/ui/modal/alert/Modal.tsx (1)

92-99: λ²„νŠΌ μ ‘κ·Όμ„± 및 의미둠적 λ§ˆν¬μ—… κ°œμ„  ν•„μš”

λ²„νŠΌμ— μ μ ˆν•œ aria-label이 μ—†μœΌλ©°, 확인 λ²„νŠΌμ˜ μ˜λ―Έκ°€ 뢈λͺ…ν™•ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

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

 <Button 
   onClick={onClose} 
   variant={'outline'} 
   type="button"
+  aria-label={`λͺ¨λ‹¬ ${cancelText}`}
 >
   {cancelText}
 </Button>
 <Button 
   onClick={onConfirm} 
   type="button"
+  aria-label={`λͺ¨λ‹¬ ${confirmText}`}
 >
   {confirmText}
 </Button>
πŸ“œ Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 4a4e250 and 99ed713.

πŸ“’ Files selected for processing (4)
  • src/app/preview/modal/Some.tsx (1 hunks)
  • src/app/preview/modal/page.tsx (1 hunks)
  • src/components/ui/modal/alert/Modal.tsx (1 hunks)
  • tailwind.config.ts (2 hunks)
🧰 Additional context used
πŸͺ› ESLint
tailwind.config.ts

[error] 18-18: Delete Β·

(prettier/prettier)

⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: check
πŸ”‡ Additional comments (2)
src/app/preview/modal/Some.tsx (1)

90-103: 색상 μ ‘κ·Όμ„± 검증 ν•„μš”

μƒνƒœ ν‘œμ‹œ 색상이 μ ‘κ·Όμ„± 기쀀을 λ§Œμ‘±ν•˜λŠ”μ§€ 확인이 ν•„μš”ν•©λ‹ˆλ‹€.

λ‹€μŒ 슀크립트λ₯Ό μ‹€ν–‰ν•˜μ—¬ 색상 λŒ€λΉ„λ₯Ό ν™•μΈν•˜μ„Έμš”:

#!/usr/bin/python3
import colorsys
import math

def calculate_contrast_ratio(color1, color2):
    # Convert hex to RGB
    def hex_to_rgb(hex_color):
        hex_color = hex_color.lstrip('#')
        return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
    
    # Calculate relative luminance
    def get_luminance(rgb):
        rgb = [v/255 for v in rgb]
        rgb = [v/12.92 if v <= 0.03928 else ((v+0.055)/1.055)**2.4 for v in rgb]
        return 0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2]
    
    rgb1 = hex_to_rgb(color1)
    rgb2 = hex_to_rgb(color2)
    
    l1 = get_luminance(rgb1)
    l2 = get_luminance(rgb2)
    
    ratio = (max(l1, l2) + 0.05) / (min(l1, l2) + 0.05)
    return ratio

# Background color
bg = '#0F0F0F'

# Status colors
colors = {
    '승인': '#22C55E',  # green-600
    'λŒ€κΈ°': '#CA8A04',  # yellow-600
    '거절': '#DC2626',  # red-600
    '강퇴': '#4B5563'   # gray-600
}

print("색상 λŒ€λΉ„μœ¨ 검사 κ²°κ³Ό:")
for status, color in colors.items():
    ratio = calculate_contrast_ratio(bg, color)
    meets_wcag = "μΆ©μ‘±" if ratio >= 4.5 else "λ―ΈμΆ©μ‘±"
    print(f"{status}: {ratio:.2f}:1 (WCAG AA κΈ°μ€€ {meets_wcag})")
tailwind.config.ts (1)

18-18: 색상 κ°’ ν¬λ§·νŒ… κ°œμ„ λ¨

BG_2 색상 값이 μ˜¬λ°”λ₯Έ HEX ν˜•μ‹μœΌλ‘œ μˆ˜μ •λ˜μ—ˆμŠ΅λ‹ˆλ‹€. λΆˆν•„μš”ν•œ 곡백도 μ œκ±°λ˜μ—ˆμŠ΅λ‹ˆλ‹€.

Also applies to: 35-35

🧰 Tools
πŸͺ› ESLint

[error] 18-18: Delete Β·

(prettier/prettier)

Comment on lines 17 to 40
<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>
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.

Comment on lines 75 to 82
<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.

Comment on lines 84 to 89
<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;

Comment on lines +82 to +85
const Some = () => {
const [isSecondModalOpen, setIsSecondModalOpen] = useState(false);
const [selectedUser, setSelectedUser] = useState<UserData | null>(null);

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);

Comment on lines +111 to +135
<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>
))}
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.

Copy link
Contributor

@dbswl701 dbswl701 left a comment

Choose a reason for hiding this comment

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

footer둜 λ²„νŠΌμ΄ ν•˜λ‚˜λ§Œ μžˆλŠ” 경우(yes)도 있고 λ²„νŠΌμ΄ 2개인 경우(yes, no)도 있고 λ²„νŠΌμ΄ μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” 경우(맴버 리슀트)도 μžˆμ–΄μ„œ λͺ¨λ“  κ²½μš°κ°€ λ‹€ κ³ λ €κ°€ λ˜μ–΄μ•Ό ν•  것 κ°™μŠ΅λ‹ˆλ‹€.

@github-actions
Copy link

CI Status Report

검사 κ²°κ³Ό

  • Lint: βœ… success
  • Format: βœ… success
  • Type Check: βœ… success
  • Tests: βœ… success
  • Build: βœ… success

βœ… λͺ¨λ“  검사가 ν†΅κ³Όλ˜μ—ˆμŠ΅λ‹ˆλ‹€.

@clyde-yoonjae
Copy link
Contributor Author

footer둜 λ²„νŠΌμ΄ ν•˜λ‚˜λ§Œ μžˆλŠ” 경우(yes)도 있고 λ²„νŠΌμ΄ 2개인 경우(yes, no)도 있고 λ²„νŠΌμ΄ μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” 경우(맴버 리슀트)도 μžˆμ–΄μ„œ λͺ¨λ“  κ²½μš°κ°€ λ‹€ κ³ λ €κ°€ λ˜μ–΄μ•Ό ν•  것 κ°™μŠ΅λ‹ˆλ‹€.

chore(DEVING-38): λͺ¨λ‹¬ 경둜 μˆ˜μ • 및 backdrop roleμˆ˜μ • ν•΄λ‹Ή 컀밋에 λ°˜μ˜ν–ˆμŠ΅λ‹ˆλ‹€!

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

πŸ“œ Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 99ed713 and 281d86c.

πŸ“’ Files selected for processing (3)
  • src/app/preview/modal/Some.tsx (1 hunks)
  • src/app/preview/modal/page.tsx (1 hunks)
  • src/components/ui/modal/Modal.tsx (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
src/app/preview/modal/page.tsx (3)
Learnt from: clyde-yoonjae
PR: MoimService/Moim-FE#14
File: src/app/preview/modal/page.tsx:17-40
Timestamp: 2025-02-19T02:34:10.468Z
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.468Z
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 관계 속성듀이 μ˜¬λ°”λ₯΄κ²Œ μž‘λ™ν•˜κΈ° μœ„ν•΄ ν•„μš”ν•©λ‹ˆλ‹€.
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.731Z
Learning: λͺ¨λ‹¬ κ΅¬ν˜„μ‹œ 접근성을 μœ„ν•œ ARIA 속성 κ°€μ΄λ“œ:
1. Backdrop divλŠ” `aria-hidden="true"`λ₯Ό μ‚¬μš©ν•˜κ³  `tabIndex`λ‚˜ `role` 속성을 μ‚¬μš©ν•˜μ§€ μ•ŠμŒ
2. λͺ¨λ‹¬ μ»¨ν…Œμ΄λ„ˆλŠ” `role="dialog"`, `aria-modal="true"`, `aria-labelledby` 속성을 포함해야 함
3. 슀크린 리더λ₯Ό μœ„ν•œ 제λͺ©μ„ μ œκ³΅ν•˜κ³  `aria-labelledby`둜 μ—°κ²°ν•΄μ•Ό 함
πŸ”‡ Additional comments (3)
src/app/preview/modal/page.tsx (1)

19-21: μ ‘κ·Όμ„± 속성 μΆ”κ°€ ν•„μš”

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

src/app/preview/modal/Some.tsx (2)

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

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


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

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

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

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

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

Comment on lines +4 to +15
interface AlertModalProps {
isOpen: boolean;
onClose: () => void;
onConfirm?: () => void;
confirmText?: string;
cancelText?: string;
children: React.ReactNode;
modalClassName?: string;
contentClassName?: string;
buttonClassName?: string;
closeOnly?: boolean;
}
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;
}

Comment on lines +88 to +122
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>
);
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>
 );

Comment on lines +74 to +84
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();
}
};
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>
);
};

Comment on lines +137 to +153
<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>
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>

@dbswl701 dbswl701 self-requested a review February 19, 2025 04:27
Copy link
Contributor

@dbswl701 dbswl701 left a comment

Choose a reason for hiding this comment

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

κ³ μƒν•˜μ…¨μŠ΅λ‹ˆλ‹€!

@clyde-yoonjae clyde-yoonjae merged commit bb582fc into feat/component/DEVING-9 Feb 19, 2025
7 checks passed
@clyde-yoonjae clyde-yoonjae deleted the feat/component/modal/DEVING-38 branch February 19, 2025 04:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants