Skip to content

관리자 유저 관리 페이지 생성#272

Merged
kimnamheeee merged 4 commits into
devfrom
add-admin-users-page
Jan 28, 2026
Merged

관리자 유저 관리 페이지 생성#272
kimnamheeee merged 4 commits into
devfrom
add-admin-users-page

Conversation

@kimnamheeee

@kimnamheeee kimnamheeee commented Jan 28, 2026

Copy link
Copy Markdown
Contributor

📝 설명

🛠️ 주요 변경 사항

리뷰 시 고려해야 할 사항

Summary by CodeRabbit

릴리스 노트

  • New Features

    • 관리자 사용자 관리 페이지 추가: 검색, 페이지 네비게이션, 대량 차단/차단 해제 기능 지원
    • 사용자 목록 필터링 및 선택 관리 기능 구현
    • 사용자 차단/차단 해제 확인 모달 추가
  • Style

    • 체크박스 스타일 개선
    • 관리자 신고접수 페이지 헤더 스타일 업데이트

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai

coderabbitai Bot commented Jan 28, 2026

Copy link
Copy Markdown

Walkthrough

관리자 사용자 관리 페이지를 새로 구현했습니다. 사용자 검색, 차단/차단 해제, 페이지 네이션 기능을 포함하는 완전한 CRUD 인터페이스를 추가했으며, 관련 API 엔드포인트, React Query 훅, UI 컴포넌트들을 함께 구성했습니다.

Changes

코호트 / 파일 변경 요약
관리자 사용자 페이지 구현
service/app/src/app/admin/users/page.tsx
플레이스홀더 렌더를 완전한 관리자 사용자 페이지로 교체. 페이지네이션, 검색, 선택 상태 관리, 차단/해제 작업 포함
API 클라이언트
service/app/src/entities/report/api/getAdminUsers.ts
관리자 사용자 목록 조회 API 함수 및 파라미터 인터페이스 추가
React Query 훅
service/app/src/entities/report/hooks/useAdminUsers.ts
관리자 사용자 데이터 조회 및 페이지네이션 상태 관리 훅 추가
데이터 타입
service/app/src/entities/report/model/types.ts
AdminUser, AdminUsersResponse 인터페이스 및 Zod 스키마 추가
차단 기능 API
service/app/src/entities/suspension/api/unblockUsers.ts
사용자 차단 해제 API 함수 추가
차단 기능 타입
service/app/src/entities/suspension/model/types.ts
UnblockUsersRequest 요청 스키마 및 타입 추가
UI 테이블 컴포넌트
service/app/src/widgets/admin/users-table/ui/UsersTable.tsx,
service/app/src/widgets/admin/users-table/ui/UserRow.tsx,
service/app/src/widgets/admin/users-table/ui/UsersPageHeader.tsx
사용자 테이블, 행, 페이지 헤더 컴포넌트 구현
선택 및 차단 훅
service/app/src/widgets/admin/users-table/hooks/useEmailSelection.ts,
service/app/src/widgets/admin/users-table/hooks/useUserBlockMutations.ts
이메일 선택 상태 및 차단/해제 뮤테이션 훅 추가
레이아웃 컴포넌트
service/app/src/shared/ui/admin-layout/AdminPageLayout.tsx
페이지네이션이 포함된 관리자 페이지 레이아웃 컴포넌트 추가
공개 API 인덱스 업데이트
service/app/src/entities/report/api/index.ts,
service/app/src/entities/report/hooks/index.ts,
service/app/src/entities/suspension/api/index.ts,
service/app/src/widgets/admin/users-table/index.ts,
service/app/src/shared/ui/admin-layout/index.ts
새로운 모듈들의 재-내보내기 추가
모의 데이터 핸들러
service/app/src/mocks/handlers/admin.ts
관리자 사용자 조회, 차단, 차단 해제 엔드포인트 모의 구현 추가
리포트 페이지 스타일
service/app/src/app/admin/reports/page.tsx
h1 요소에 shrink-0text-text-secondary CSS 클래스 추가
체크박스 스타일
shared/design/src/components/checkBox/index.tsx
레이블 컨테이너 및 입력 요소 스타일 클래스 수정

Sequence Diagram(s)

sequenceDiagram
    participant User as 사용자
    participant UI as 관리자 페이지
    participant Query as React Query
    participant API as API 클라이언트
    participant Server as 백엔드/Mock

    User->>UI: 페이지 접속
    UI->>Query: useAdminUsers 호출 (page=0)
    Query->>API: getAdminUsers({ page: 0 })
    API->>Server: GET /admin/users?page=0
    Server-->>API: AdminUsersResponse
    API-->>Query: 데이터 반환
    Query-->>UI: users, currentPage, totalPages, isLoading
    UI->>UI: 사용자 목록 렌더링

    User->>UI: 검색어 입력 후 Enter
    UI->>UI: searchQuery 상태 업데이트, 필터링 적용
    UI->>UI: 필터된 사용자 목록 표시

    User->>UI: 사용자 체크박스 선택
    UI->>UI: useEmailSelection으로 선택 상태 관리

    User->>UI: '차단' 버튼 클릭
    UI->>UI: 확인 모달 표시
    User->>UI: 확인
    UI->>Query: blockMutation (selectedEmails)
    Query->>API: blockUsers({ banList: [...] })
    API->>Server: POST /admin/users/block
    Server-->>API: 성공
    API-->>Query: 응답
    Query->>Query: "adminUsers" 캐시 무효화
    UI->>Query: useAdminUsers 자동 갱신
    Query->>API: getAdminUsers 다시 호출
    API->>Server: GET /admin/users?page=currentPage
    Server-->>API: 갱신된 AdminUsersResponse
    API-->>Query: 데이터 반환
    Query-->>UI: 업데이트된 목록
    UI->>UI: 선택 상태 초기화, 목록 갱신

    User->>UI: '차단 해제' 버튼 클릭
    UI->>UI: 확인 모달 표시
    User->>UI: 확인
    UI->>Query: unblockMutation (selectedEmails)
    Query->>API: unblockUsers({ emails: [...] })
    API->>Server: POST /admin/users/unblock
    Server-->>API: 성공
    API-->>Query: 응답
    Query->>Query: "adminUsers" 캐시 무효화
    UI->>Query: useAdminUsers 자동 갱신
    Query->>API: getAdminUsers 다시 호출
    API->>Server: GET /admin/users
    Server-->>API: 갱신된 AdminUsersResponse
    API-->>Query: 데이터 반환
    Query-->>UI: 업데이트된 목록
    UI->>UI: 선택 상태 초기화, 목록 갱신
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • #270: 동일한 관리자 리포트 페이지(service/app/src/app/admin/reports/page.tsx)의 스타일 조정으로 연관
  • #268: 체크박스 컴포넌트(shared/design/src/components/checkBox/index.tsx)의 스타일 변경으로 연관
  • #265: 게임 리포트 모델의 reportReasonCodeSchema를 재사용하여 관리자 사용자 타입 정의에서 연관

Suggested labels

✨feature, 👩🏻‍💻frontend

Suggested reviewers

  • whdgur5717

Poem

🐰 사용자 페이지 뙇! 새로 나타났네
검색도 되고 차단도 척척 ✨
테이블 행들 줄지어 선다
페이지네이션은 우아하게,
관리자의 일은 이제 한결 수월해! 🎉

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description check ⚠️ Warning PR 설명이 제공된 템플릿 구조를 따르고 있지만, 모든 필수 섹션이 실제 내용 없이 주석으로만 남겨져 있어 완성되지 않았습니다. 📝 설명과 🛠️ 주요 변경 사항 섹션을 구체적인 내용으로 작성하고, commit sha 정보를 포함하여 작업 내용을 명확히 기술해주세요.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 주요 변경사항을 명확하게 요약하고 있습니다. 관리자 유저 관리 페이지 생성이라는 핵심 기능이 간결하고 이해하기 쉬운 형태로 표현되어 있습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

Copy link
Copy Markdown

테스트 실패

테스트가 실패했습니다. Actions 로그를 확인해주세요.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@service/app/src/app/admin/users/page.tsx`:
- Around line 35-48: The client-side filtering in filteredUsers (useMemo) using
searchQuery/users causes pagination mismatch with the backend-derived
totalPages; either move search to the backend by updating the API calls to
accept a search query and page parameter (so the data returned and totalPages
reflect the filtered results), or change the UI logic in page.tsx to detect
active searchQuery and hide/disable pagination controls (totalPages) while using
filteredUsers; locate and update the useMemo for filteredUsers and userEmails as
well as any component or prop that renders totalPages to implement the chosen
approach.

In `@service/app/src/entities/report/api/getAdminUsers.ts`:
- Around line 12-17: The current getAdminUsers implementation uses
response.json().then(...) which is inconsistent with getReportDetail and
getAdminReports; update getAdminUsers (and similarly getAdminReports) to use
const res = await response.json() and then return res.data, ensuring you await
response.json() instead of chaining .then; locate the fetch call in
getAdminUsers (the
fetchClient.get<AdminUsersResponse>(`admin/users${queryString}`) and the
variable response) and replace the .then pattern with the awaited assignment to
res and return res.data, keeping the existing page/queryString logic intact.

In `@service/app/src/mocks/handlers/admin.ts`:
- Around line 58-61: The blockedEmails Set is initialized empty while
generateMockUsers may produce users with a blocked flag, causing inconsistent
responses; inside createAdminHandlers after usersStore is created, iterate
usersStore (AdminUser objects) and add each user's email to blockedEmails when
user.blocked (or equivalent property) is true so blockedEmails and usersStore
start synchronized.

In `@shared/design/src/components/checkBox/index.tsx`:
- Line 97: The hidden input in the CheckBox component currently uses the classes
in className={cn("peer absolute size-0 opacity-0", className)} which can hide it
from screen readers; replace those classes with the accessible pattern used
elsewhere (use "peer sr-only") so the line becomes className={cn("peer sr-only",
className)} to match radioField and file upload components and ensure
WCAG-compatible screen-reader accessibility.
🧹 Nitpick comments (7)
service/app/src/entities/suspension/model/types.ts (1)

21-23: 이메일 형식 검증 추가 고려

현재 z.string().min(1)은 빈 문자열만 방지합니다. Zod 4에서는 z.email()을 사용하여 이메일 형식을 검증할 수 있습니다. 기존 banListItemSchema와의 일관성을 위해 두 스키마 모두에 이메일 검증을 추가하는 것을 고려해 보세요.

♻️ 제안된 수정
 export const unblockUsersRequestSchema = z.object({
-  emails: z.array(z.string().min(1)).min(1),
+  emails: z.array(z.email()).min(1),
 })
service/app/src/widgets/admin/users-table/ui/UsersPageHeader.tsx (2)

25-30: 검색 버튼 추가 고려 (선택 사항)

현재 Enter 키로만 검색이 트리거됩니다. 접근성과 사용성 향상을 위해 검색 아이콘 버튼을 추가하거나, 입력 필드의 leftIcon 타입에 맞는 검색 아이콘에 클릭 이벤트를 연결하는 것을 고려해 보세요.


54-61: 선택된 사용자 없을 때 버튼 비활성화 고려

onBlockonUnblock 버튼이 선택 상태와 관계없이 항상 활성화되어 있습니다. 부모 컴포넌트에서 선택 상태를 관리하므로, disabled prop을 추가하여 선택된 사용자가 없을 때 버튼을 비활성화하는 것을 고려해 보세요.

♻️ 제안된 수정
 interface UsersPageHeaderProps {
   onSearch: (query: string) => void
   onBlock: () => void
   onUnblock: () => void
+  isSelectionEmpty?: boolean
 }

 export const UsersPageHeader = ({
   onSearch,
   onBlock,
   onUnblock,
+  isSelectionEmpty = true,
 }: UsersPageHeaderProps) => {
       <div className="flex shrink-0 gap-16">
-        <DestructiveSolidBoxButton size="lg" onClick={onBlock}>
+        <DestructiveSolidBoxButton size="lg" onClick={onBlock} disabled={isSelectionEmpty}>
           차단
         </DestructiveSolidBoxButton>
-        <PrimaryBoxButton size="lg" onClick={onUnblock}>
+        <PrimaryBoxButton size="lg" onClick={onUnblock} disabled={isSelectionEmpty}>
           차단 해제
         </PrimaryBoxButton>
       </div>
service/app/src/shared/ui/admin-layout/AdminPageLayout.tsx (1)

1-38: 파일명을 camelCase로 변경하는 것을 권장합니다.

코딩 가이드라인에 따르면 파일명은 camelCase여야 합니다. AdminPageLayout.tsxadminPageLayout.tsx로 변경을 고려해 주세요.

그 외 컴포넌트 로직과 페이지네이션 조건부 렌더링은 잘 구현되어 있습니다.

As per coding guidelines: "All file names must be camelCase (not PascalCase or kebab-case)"

service/app/src/widgets/admin/users-table/ui/UsersTable.tsx (1)

1-81: 파일명을 camelCase로 변경하는 것을 권장합니다.

UsersTable.tsxusersTable.tsx로 변경을 고려해 주세요.

컴포넌트 구현은 깔끔하며, ARIA 레이블 적용과 select-all 로직이 잘 작성되어 있습니다.

As per coding guidelines: "All file names must be camelCase (not PascalCase or kebab-case)"

service/app/src/widgets/admin/users-table/ui/UserRow.tsx (1)

1-60: 파일명을 camelCase로 변경하는 것을 권장합니다.

UserRow.tsxuserRow.tsx로 변경을 고려해 주세요.

memo 사용과 blockReasonLabel null 처리가 적절하게 구현되어 있습니다.

As per coding guidelines: "All file names must be camelCase (not PascalCase or kebab-case)"

service/app/src/entities/report/hooks/useAdminUsers.ts (1)

24-33: queryFn 단순화를 권장합니다.

async 래퍼가 불필요합니다. getAdminUsers를 직접 전달할 수 있습니다:

♻️ 제안된 리팩토링
   const { data, isLoading, error } = useQuery({
     queryKey: ["adminUsers", { page }] as const,
-    queryFn: async () => {
-      const response = await getAdminUsers({ page })
-      return response
-    },
+    queryFn: () => getAdminUsers({ page }),
     enabled,
     staleTime: 5 * 60 * 1000,
     retry: 2,
   })

Comment on lines +35 to +48
const filteredUsers = useMemo(() => {
if (!searchQuery) return users
const query = searchQuery.toLowerCase()
return users.filter(
(user) =>
user.nickname.toLowerCase().includes(query) ||
user.email.toLowerCase().includes(query),
)
}, [users, searchQuery])

const userEmails = useMemo(
() => filteredUsers.map((u) => u.email),
[filteredUsers],
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

프론트엔드 필터링과 백엔드 페이지네이션 간의 불일치 문제가 있습니다.

filteredUsers는 클라이언트에서 필터링되지만, totalPages는 백엔드 전체 데이터 기준입니다. 검색 시 필터링된 결과 수와 페이지네이션이 맞지 않아 사용자 혼란을 야기할 수 있습니다.

예: 100명의 유저가 10페이지에 걸쳐 있을 때 검색으로 2명만 필터링되어도 여전히 10페이지가 표시됩니다.

권장 해결 방안:

  1. 검색을 백엔드 API로 이동 (?search=query&page=n)
  2. 또는 검색 중에는 페이지네이션을 숨기거나 비활성화

Also applies to: 94-96

🤖 Prompt for AI Agents
In `@service/app/src/app/admin/users/page.tsx` around lines 35 - 48, The
client-side filtering in filteredUsers (useMemo) using searchQuery/users causes
pagination mismatch with the backend-derived totalPages; either move search to
the backend by updating the API calls to accept a search query and page
parameter (so the data returned and totalPages reflect the filtered results), or
change the UI logic in page.tsx to detect active searchQuery and hide/disable
pagination controls (totalPages) while using filteredUsers; locate and update
the useMemo for filteredUsers and userEmails as well as any component or prop
that renders totalPages to implement the chosen approach.

Comment on lines +12 to +17
const { page = 0 } = params
const queryString = page > 0 ? `?page=${page}` : ""
const response = await fetchClient.get<AdminUsersResponse>(
`admin/users${queryString}`,
)
return response.json().then((res) => res.data)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the actual file
cat -n service/app/src/entities/report/api/getAdminUsers.ts

Repository: JECT-Study/JECT-5-FE

Length of output: 704


🏁 Script executed:

# Check for similar query string patterns in other API files
rg -n "queryString.*page|page.*queryString" --type=ts service/app/src/entities/ -A 2 -B 2

Repository: JECT-Study/JECT-5-FE

Length of output: 1070


🏁 Script executed:

# Look for pagination patterns in API files
rg -n "page.*\?" --type=ts service/app/src/entities/ | head -20

Repository: JECT-Study/JECT-5-FE

Length of output: 909


🏁 Script executed:

# Check async patterns - .json().then() vs await in API files
rg -n "\.json\(\)\.then\(" --type=ts service/app/src/entities/ -A 1 -B 1

Repository: JECT-Study/JECT-5-FE

Length of output: 518


🏁 Script executed:

# Check for other response.json() patterns to understand codebase conventions
rg -n "response\.json\(\)" --type=ts service/app/src/entities/ -A 2 | head -40

Repository: JECT-Study/JECT-5-FE

Length of output: 2086


response.json().then() 패턴을 await로 통일하세요.

같은 report 엔티티의 getReportDetail.ts에서는 const res = await response.json() 패턴을 사용하고 있으므로, 일관성을 위해 이 파일도 동일하게 변경해야 합니다. getAdminReports.ts도 같은 패턴을 사용하므로 함께 수정하세요.

♻️ 제안된 수정
-  return response.json().then((res) => res.data)
+  const res = await response.json()
+  return res.data

참고: page > 0 조건의 의도(0-인덱싱 여부)는 백엔드 API 사양과 검증 필요합니다.

📝 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 { page = 0 } = params
const queryString = page > 0 ? `?page=${page}` : ""
const response = await fetchClient.get<AdminUsersResponse>(
`admin/users${queryString}`,
)
return response.json().then((res) => res.data)
const { page = 0 } = params
const queryString = page > 0 ? `?page=${page}` : ""
const response = await fetchClient.get<AdminUsersResponse>(
`admin/users${queryString}`,
)
const res = await response.json()
return res.data
🤖 Prompt for AI Agents
In `@service/app/src/entities/report/api/getAdminUsers.ts` around lines 12 - 17,
The current getAdminUsers implementation uses response.json().then(...) which is
inconsistent with getReportDetail and getAdminReports; update getAdminUsers (and
similarly getAdminReports) to use const res = await response.json() and then
return res.data, ensuring you await response.json() instead of chaining .then;
locate the fetch call in getAdminUsers (the
fetchClient.get<AdminUsersResponse>(`admin/users${queryString}`) and the
variable response) and replace the .then pattern with the awaited assignment to
res and return res.data, keeping the existing page/queryString logic intact.

Comment on lines 58 to 61
export const createAdminHandlers = (initialReports: AdminReport[]) => {
const store: AdminReport[] = structuredClone(initialReports)
const usersStore: AdminUser[] = generateMockUsers(32)
const blockedEmails = new Set<string>()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

초기 blocked 상태와 blockedEmails 동기화가 필요합니다.

usersStore에 이미 차단된 유저가 존재하는데 blockedEmails는 빈 상태라, 일부 응답에서 차단 상태가 불일치할 수 있습니다. 초기화 시 동기화해 두는 편이 안전합니다.

🔧 제안 수정안
-  const usersStore: AdminUser[] = generateMockUsers(32)
-  const blockedEmails = new Set<string>()
+  const usersStore: AdminUser[] = generateMockUsers(32)
+  const blockedEmails = new Set(
+    usersStore.filter((user) => user.blocked).map((user) => user.email),
+  )
🤖 Prompt for AI Agents
In `@service/app/src/mocks/handlers/admin.ts` around lines 58 - 61, The
blockedEmails Set is initialized empty while generateMockUsers may produce users
with a blocked flag, causing inconsistent responses; inside createAdminHandlers
after usersStore is created, iterate usersStore (AdminUser objects) and add each
user's email to blockedEmails when user.blocked (or equivalent property) is true
so blockedEmails and usersStore start synchronized.

id={id}
type="checkbox"
className={cn("peer sr-only", className)}
className={cn("peer absolute size-0 opacity-0", className)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

cat -n shared/design/src/components/checkBox/index.tsx | head -100 | tail -20

Repository: JECT-Study/JECT-5-FE

Length of output: 744


🌐 Web query:

Tailwind CSS sr-only vs absolute size-0 opacity-0 accessibility screen reader hidden input

💡 Result:

What sr-only does (Tailwind)

Tailwind’s sr-only is the classic “visually hidden but still in the accessibility tree” pattern: it keeps the element present for screen readers while making it effectively invisible on screen using position:absolute, 1px sizing, clipping, and overflow hidden. (v3.tailwindcss.com)

Why absolute + size-0 is risky

Setting an element to width: 0 / height: 0 (or similar “0px sizing” tricks) is not recommended if you want screen readers to read it—many AT/browser combinations will effectively ignore it. (webaim.org)

opacity-0 (and why it can be a problem)

opacity: 0 generally does not remove the element from the accessibility tree; it can still be focusable/tabbable and announced, which can create “ghost” focus targets if you don’t manage focus and interaction carefully. (ionic.io)

What actually hides from screen readers

  • display: none / visibility: hidden remove content from screen readers (and the accessibility tree). (developer.mozilla.org)
  • aria-hidden="true" removes content from the accessibility API, and should not be used on focusable elements. (developer.mozilla.org)

Practical guidance (common “hidden input” cases)

  • Hidden labels/instructions (text): use sr-only. (v3.tailwindcss.com)
  • Custom checkbox/radio (real input should remain accessible): avoid size-0; use a “visually hidden” pattern like sr-only so the input remains operable and announced. (v3.tailwindcss.com)
  • Hiding something visually but keeping it available to AT: prefer clipping/off-screen techniques (sr-only) over opacity-0 unless you intentionally want it focusable while invisible. (v3.tailwindcss.com)

Citations:


🌐 Web query:

sr-only class screen reader accessibility best practices form inputs

💡 Result:

Best practices for using an sr-only / “visually hidden” class with form inputs

  • Prefer a real <label> for every input and programmatically associate it with the control using for + matching id. If you don’t want the label visible, hide the label visually (e.g., with sr-only), but keep it in the DOM. [1]
  • Don’t hide labels with display:none or visibility:hidden—that removes them from the accessibility tree for assistive tech. Use a “visually hidden” CSS technique instead. [1]
  • Avoid relying on placeholder as the label. Placeholders are fallback naming, can be low quality, and disappear when users type; use <label> as the primary method. [3]
  • Use aria-label only when the label is genuinely clear from surrounding context and you intentionally have no visible label; otherwise a <label> is usually better. [1]
  • If there is visible label text, make sure the accessible name matches/includes it (important for speech recognition; WCAG “Label in Name”). Don’t override a visible label with a different aria-label. [2], [4]

Common “sr-only / visually hidden” CSS pattern (example)

.sr-only {
  border: 0;
  clip: rect(0 0 0 0);
  height: 1px;
  margin: -1px;
  overflow: hidden;
  padding: 0;
  position: absolute;
  width: 1px;
}

(Concept shown in WAI’s “visually hidden” label example.) [1]

Example: hidden label, accessible input

<label for="email" class="sr-only">Email address</label>
<input id="email" name="email" type="email" autocomplete="email">

[1]

Sources: W3C WAI Forms tutorial (labels + hiding labels correctly) [1]; W3C WCAG techniques for “Label in Name” [2], [4].


🏁 Script executed:

rg "sr-only" shared/design/src/components/ -A 2 -B 2

Repository: JECT-Study/JECT-5-FE

Length of output: 2038


커스텀 체크박스의 숨겨진 입력 요소는 sr-only를 사용해야 합니다

absolute size-0 opacity-0은 스크린 리더에서 요소를 무시하는 경향이 있어 접근성 기준 (WCAG)을 충족하지 않습니다. 같은 디자인 시스템 내 라디오버튼 컴포넌트(radioField/index.tsx)와 파일 업로드 컴포넌트에서는 peer sr-only를 사용 중이므로, 체크박스도 동일하게 변경하세요: className={cn("peer sr-only", className)}

🤖 Prompt for AI Agents
In `@shared/design/src/components/checkBox/index.tsx` at line 97, The hidden input
in the CheckBox component currently uses the classes in className={cn("peer
absolute size-0 opacity-0", className)} which can hide it from screen readers;
replace those classes with the accessible pattern used elsewhere (use "peer
sr-only") so the line becomes className={cn("peer sr-only", className)} to match
radioField and file upload components and ensure WCAG-compatible screen-reader
accessibility.

@github-actions

Copy link
Copy Markdown

Deploy preview for re-creation ready!

✅ Preview
https://re-creation-e4iwjm18g-whdgur5717s-projects.vercel.app

Built with commit f4d5b35.
This pull request is being automatically deployed with vercel-action

@kimnamheeee kimnamheeee merged commit 896ad74 into dev Jan 28, 2026
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant