@@ -4,13 +4,28 @@ import { composeEventHandlers } from "../utils/composeEventHandlers";
44import { useAriaIds , useAriaPress } from "@acme/react-a11y" ;
55import { createContext } from "../context/createContext" ;
66
7+ /**
8+ * Card 컴포넌트의 인터랙션 모드를 정의합니다.
9+ * - "none": 정적 카드 (클릭 불가)
10+ * - "button": 버튼처럼 동작하는 카드 (키보드/마우스 지원)
11+ * - "link": 링크처럼 동작하는 카드 (키보드/마우스 지원)
12+ */
713type CardAction = "none" | "button" | "link" ;
814
15+ /**
16+ * 다형성 컴포넌트를 위한 props 타입
17+ * - as: 렌더링할 HTML 요소나 React 컴포넌트 지정
18+ * - asChild: true일 때 자식 요소를 직접 렌더링 (Slot 패턴)
19+ */
920type PolymorphicProp < C extends React . ElementType > = {
1021 as ?: C ;
1122 asChild ?: boolean ;
1223} ;
1324
25+ /**
26+ * Card 컴포넌트의 기본 props 타입
27+ * 인터랙션, 접근성, 이벤트 핸들링 관련 속성들을 포함합니다.
28+ */
1429type CardBaseProps = {
1530 /** 카드가 인터랙티브(클릭/키보드 활성)한지 */
1631 action ?: CardAction ;
@@ -30,20 +45,42 @@ type CardBaseProps = {
3045 externalHandlersFirst ?: boolean ;
3146} & React . HTMLAttributes < HTMLElement > ;
3247
48+ /**
49+ * Card 컴포넌트의 전체 props 타입
50+ * 다형성 지원과 함께 HTML 속성들을 상속받습니다.
51+ */
3352export type CardProps < C extends React . ElementType = React . ElementType > =
3453 React . PropsWithChildren < CardBaseProps & PolymorphicProp < C > > &
3554 Omit <
3655 React . ComponentPropsWithoutRef < C > ,
3756 keyof CardBaseProps | "children" | "as" | "asChild"
3857 > ;
3958
59+ /**
60+ * Card Context에서 공유되는 값들
61+ * 접근성을 위한 ID들을 하위 컴포넌트들과 공유합니다.
62+ */
4063type CardContextValue = {
4164 titleId : string ;
4265 descId : string ;
4366} ;
4467
68+ /**
69+ * Card 컴포넌트의 Context 생성
70+ * 향상된 createContext 유틸리티를 사용하여 타입 안전성과 성능을 개선합니다.
71+ */
4572const [ CardProvider , useCardContext ] = createContext < CardContextValue > ( "Card" ) ;
4673
74+ /**
75+ * Card 컴포넌트의 루트 요소
76+ *
77+ * 주요 기능:
78+ * - 다형성 지원 (as, asChild props)
79+ * - 인터랙션 모드 지원 (none, button, link)
80+ * - 접근성 자동 처리 (ARIA 속성, 키보드 네비게이션)
81+ * - 이벤트 핸들러 조합 (내부/외부 핸들러 통합)
82+ * - Context를 통한 ID 공유
83+ */
4784export const CardRoot = React . forwardRef < any , CardProps > (
4885 (
4986 {
@@ -63,7 +100,8 @@ export const CardRoot = React.forwardRef<any, CardProps>(
63100 } ,
64101 ref
65102 ) => {
66- // asChild일 때 자식이 하나인지 검증
103+ // asChild 모드일 때 자식 요소 검증
104+ // Slot 패턴을 사용할 때는 정확히 하나의 React 요소가 필요합니다
67105 if ( asChild ) {
68106 if ( ! React . isValidElement ( children ) ) {
69107 throw new Error (
@@ -72,44 +110,58 @@ export const CardRoot = React.forwardRef<any, CardProps>(
72110 }
73111 }
74112
113+ // 렌더링할 요소 타입 결정
114+ // asChild가 true면 Slot 컴포넌트를, 아니면 지정된 요소나 기본 div를 사용
75115 const elementType = asChild ? Slot : ( as ?? "div" ) ;
116+
117+ // 접근성을 위한 고유 ID 생성
118+ // 외부에서 제공된 ID가 있으면 우선 사용, 없으면 자동 생성
76119 const { label : autoTitleId , desc : autoDescId } = useAriaIds ( "card" ) ;
77120 const titleId = ariaLabelledbyProp || autoTitleId ;
78121 const descId = ariaDescribedbyProp || autoDescId ;
79122
80- // a11y press (button 모드일 때 키보드/마우스 일원화)
123+ // 접근성 지원을 위한 키보드/마우스 이벤트 통합
124+ // useAriaPress 훅을 사용하여 키보드와 마우스 이벤트를 일관되게 처리
81125 const press = useAriaPress ( {
82126 disabled : disabled || action === "none" ,
83127 onPress : onPress ? ( t ) => onPress ( t ) : undefined ,
84128 } ) ;
85129
86- // 역할/ARIA 계산
130+ // ARIA 역할 및 인터랙션 상태 계산
131+ // action prop에 따라 적절한 ARIA 역할을 자동 설정
87132 const isButton = action === "button" ;
88133 const isLink = action === "link" ;
89134 const role =
90135 rest . role ?? ( isButton ? "button" : isLink ? "link" : undefined ) ;
91136
92- // Card 컴포넌트의 내부 기본 동작
137+ // Card 컴포넌트의 내부 기본 동작 정의
138+ // 인터랙티브 모드일 때만 내부 핸들러를 생성 (포커스 관리, 애니메이션 등)
93139 const internalOnClick =
94140 isButton || isLink
95141 ? ( e : React . MouseEvent ) => {
96142 // Card의 기본 클릭 동작 (예: 포커스 관리, 애니메이션 등)
143+ // 향후 확장 가능한 내부 로직
97144 }
98145 : undefined ;
99146
100147 const internalOnKeyDown =
101148 isButton || isLink
102149 ? ( e : React . KeyboardEvent ) => {
103150 // Card의 기본 키보드 동작
151+ // 향후 확장 가능한 내부 로직
104152 }
105153 : undefined ;
106154
107- // 외부 onPress를 처리하는 핸들러
155+ // 외부 onPress 이벤트를 처리하는 핸들러
156+ // useAriaPress를 다시 호출하여 외부 핸들러와 내부 핸들러를 분리
108157 const pressHandlers = useAriaPress ( {
109158 disabled : disabled || action === "none" ,
110159 onPress : onPress ? ( t ) => onPress ( t ) : undefined ,
111160 } ) ;
112161
162+ // 이벤트 핸들러 조합
163+ // 내부 핸들러와 외부 핸들러를 composeEventHandlers로 통합
164+ // externalHandlersFirst 옵션으로 실행 순서 제어 가능
113165 const handleClick = composeEventHandlers (
114166 internalOnClick ,
115167 pressHandlers . onClick ,
@@ -121,35 +173,48 @@ export const CardRoot = React.forwardRef<any, CardProps>(
121173 { externalFirst : ! ! externalHandlersFirst }
122174 ) ;
123175
124- // 네이티브 button 기본 submit 방지
176+ // 네이티브 button 요소의 기본 submit 동작 방지
177+ // button 요소로 렌더링될 때 type="button"을 명시적으로 설정
125178 const typeProp =
126179 elementType === "button" && isButton ? { type : "button" } : { } ;
127180
181+ // Context Provider로 ID들을 하위 컴포넌트들과 공유
182+ // CardTitle, CardDescription 등이 이 ID들을 사용하여 접근성 연결
128183 return (
129184 < CardProvider titleId = { titleId } descId = { descId } >
130185 { React . createElement (
131186 elementType ,
132187 {
188+ // 기본 HTML 속성들 전달
133189 ...rest ,
190+ // 인터랙티브 모드일 때만 press 관련 속성들 추가
134191 ...( isButton || isLink ? press : { } ) ,
135192 ref,
193+ // button 요소의 기본 submit 방지
136194 ...typeProp ,
195+ // ARIA 역할 설정
137196 role,
197+ // 접근성 속성들
138198 "aria-disabled" : disabled || undefined ,
139199 "aria-pressed" :
140200 isButton && typeof pressed === "boolean" ? pressed : undefined ,
141201 "aria-labelledby" : titleId ,
142202 "aria-describedby" : descId ,
203+ // 키보드 네비게이션을 위한 tabIndex 설정
143204 tabIndex :
144205 rest . tabIndex ??
145206 ( isButton || isLink ? press . tabIndex : undefined ) ,
207+ // 조합된 이벤트 핸들러들
146208 onClick : handleClick ,
147209 onKeyDown : handleKeyDown ,
210+ // 포커스 표시를 위한 데이터 속성
148211 "data-focus-visible" : "" ,
149- // asChild일 때는 기본 스타일을 적용하지 않음 (레이아웃 깨짐 방지)
212+ // 조건부 스타일 적용
213+ // asChild일 때는 display: block 추가 (레이아웃 깨짐 방지)
214+ // 일반 모드일 때는 기본 스타일만 적용
150215 style : asChild
151216 ? {
152- display : "block" ,
217+ display : "block" , // asChild일 때만 추가
153218 outline : "none" ,
154219 borderRadius : "var(--ds-radius-2, 12px)" ,
155220 background : "var(--ds-semantic-color-bg-layer-default)" ,
@@ -180,7 +245,14 @@ export const CardRoot = React.forwardRef<any, CardProps>(
180245) ;
181246CardRoot . displayName = "Card.Root" ;
182247
183- // 슬롯들: aria-labelledby / describedby에 연결
248+ /* -------------------------------------------------------------------------------------------------
249+ * Card 하위 컴포넌트들
250+ * -----------------------------------------------------------------------------------------------*/
251+
252+ /**
253+ * Card의 헤더 영역
254+ * 다형성 지원으로 다양한 요소로 렌더링 가능
255+ */
184256export const CardHeader = React . forwardRef <
185257 HTMLElement ,
186258 React . HTMLAttributes < HTMLElement > & PolymorphicProp < any >
@@ -190,6 +262,10 @@ export const CardHeader = React.forwardRef<
190262} ) ;
191263CardHeader . displayName = "Card.Header" ;
192264
265+ /**
266+ * Card의 미디어 영역 (이미지, 비디오 등)
267+ * 다형성 지원으로 다양한 요소로 렌더링 가능
268+ */
193269export const CardMedia = React . forwardRef <
194270 HTMLElement ,
195271 React . HTMLAttributes < HTMLElement > & PolymorphicProp < any >
@@ -199,6 +275,11 @@ export const CardMedia = React.forwardRef<
199275} ) ;
200276CardMedia . displayName = "Card.Media" ;
201277
278+ /**
279+ * Card의 제목
280+ * Context에서 제공되는 titleId를 자동으로 연결하여 접근성 지원
281+ * Card.Root의 aria-labelledby와 자동 연결됨
282+ */
202283export const CardTitle = React . forwardRef <
203284 HTMLElement ,
204285 React . HTMLAttributes < HTMLElement > & PolymorphicProp < any >
@@ -209,6 +290,11 @@ export const CardTitle = React.forwardRef<
209290} ) ;
210291CardTitle . displayName = "Card.Title" ;
211292
293+ /**
294+ * Card의 설명
295+ * Context에서 제공되는 descId를 자동으로 연결하여 접근성 지원
296+ * Card.Root의 aria-describedby와 자동 연결됨
297+ */
212298export const CardDescription = React . forwardRef <
213299 HTMLElement ,
214300 React . HTMLAttributes < HTMLElement > & PolymorphicProp < any >
@@ -219,6 +305,10 @@ export const CardDescription = React.forwardRef<
219305} ) ;
220306CardDescription . displayName = "Card.Description" ;
221307
308+ /**
309+ * Card의 본문 영역
310+ * 다형성 지원으로 다양한 요소로 렌더링 가능
311+ */
222312export const CardBody = React . forwardRef <
223313 HTMLElement ,
224314 React . HTMLAttributes < HTMLElement > & PolymorphicProp < any >
@@ -228,6 +318,10 @@ export const CardBody = React.forwardRef<
228318} ) ;
229319CardBody . displayName = "Card.Body" ;
230320
321+ /**
322+ * Card의 푸터 영역
323+ * 다형성 지원으로 다양한 요소로 렌더링 가능
324+ */
231325export const CardFooter = React . forwardRef <
232326 HTMLElement ,
233327 React . HTMLAttributes < HTMLElement > & PolymorphicProp < any >
@@ -237,7 +331,24 @@ export const CardFooter = React.forwardRef<
237331} ) ;
238332CardFooter . displayName = "Card.Footer" ;
239333
240- // 네임스페이스 export (선호 시)
334+ /* -------------------------------------------------------------------------------------------------
335+ * 네임스페이스 export
336+ * -----------------------------------------------------------------------------------------------*/
337+
338+ /**
339+ * Card 컴포넌트의 네임스페이스 export
340+ *
341+ * 사용법:
342+ * ```tsx
343+ * <Card.Root>
344+ * <Card.Header>
345+ * <Card.Title>제목</Card.Title>
346+ * </Card.Header>
347+ * <Card.Body>내용</Card.Body>
348+ * <Card.Footer>푸터</Card.Footer>
349+ * </Card.Root>
350+ * ```
351+ */
241352export const Card = Object . assign ( CardRoot , {
242353 Root : CardRoot ,
243354 Header : CardHeader ,
0 commit comments