임동욱 임동욱 08-18
feat: FoxAccordion·FoxCard 추가
아코디언(194:3563 box · 194:3680 line)과 카드(441:6006)를 옮겼다.

FoxAccordion — 계열 둘이 성격이 다르다. box는 색을 깐 상자로 여백·모서리를 상자가 갖고
제목이 강조 색이며, line은 위쪽 선 하나로 나뉘는 목록형이라 바탕이 없고 제목 줄이 세로
여백을 갖는다. 크기 lg·md가 여백·글자·모서리를 함께 바꾼다.

열림은 넘기면 호출부가, 안 넘기면 컴포넌트가 쥔다 — 여러 개 중 하나만 열리게 하려면
호출부가 쥐면 된다. 닫히면 내용을 DOM에서 뺀다. 안의 초점 가능한 것들이 탭 순서에 남으면
보이지 않는 곳으로 초점이 사라진다.

FoxCard — 크기 넷은 여백과 모서리만 바꾼다. 폭은 놓이는 자리가 정한다(시안은 320px 자리에
뒀을 뿐 카드 자체의 값이 아니다). 아래쪽 동작 줄은 좌우·아래 여백만 갖는다 — 위쪽은 글
영역의 아래 여백이 이미 띄워 준다.

검증: 아코디언 16항목 중 15개 일치(나머지 1건은 내 예상값 오류였고 코드가 맞다 —
action/secondary는 투명 검정이 아니라 투명 흰색이다). 카드 18항목 전부 일치.
sass 694규칙 · tsc · eslint 통과.

승인된 비토큰 값: 그림 비율 16/9.

Co-Authored-By: Claude Opus 5 
@147dcfcb464b82f590bd5557d7104e36bfa4fc50
 
@fox/core/components/fox-accordion/fox-accordion.tsx (added)
+++ @fox/core/components/fox-accordion/fox-accordion.tsx
@@ -0,0 +1,98 @@
+"use client";
+
+import { useId, useState, type ReactNode } from "react";
+import { cx } from "../../utils";
+import { FoxCaretDownIcon, FoxCaretUpIcon } from "../../icons";
+
+/** `box`는 색을 깐 상자, `line`은 위쪽 선 하나로 나뉘는 목록형이다. */
+export type FoxAccordionType = "box" | "line";
+export type FoxAccordionSize = "lg" | "md";
+
+export interface FoxAccordionProps {
+  title: ReactNode;
+  children?: ReactNode;
+  type?: FoxAccordionType;
+  size?: FoxAccordionSize;
+  /** 제목 앞자리. 시안은 배지 묶음이다. */
+  badge?: ReactNode;
+  /** 주면 호출부가 열림을 소유한다. 없으면 컴포넌트가 스스로 여닫는다. */
+  open?: boolean;
+  defaultOpen?: boolean;
+  onOpenChange?: (open: boolean) => void;
+  /** 참이면 렌더하지 않는다(DOM에 남지 않는다). */
+  hidden?: boolean;
+  /** 배치 조정용. */
+  className?: string;
+}
+
+/**
+ * @fox 아코디언. 제목을 누르면 아래 내용이 열린다.
+ *
+ * 열림을 밖에서 쥐면 여러 개 중 하나만 열리게 하는 것도 호출부가 정할 수 있다. 넘기지
+ * 않으면 스스로 여닫는다.
+ *
+ * ⚠️ 스타일은 이 파일이 import하지 않는다 — 호스트 앱이 `@use "@fox/styles/components"`
+ * (또는 개별 파티셜)로 한 번 불러와야 한다.
+ */
+export function FoxAccordion({
+  title,
+  children,
+  type = "box",
+  size = "lg",
+  badge,
+  open,
+  defaultOpen = false,
+  onOpenChange,
+  hidden = false,
+  className,
+}: FoxAccordionProps) {
+  const autoId = useId();
+  const [inner, setInner] = useState(defaultOpen);
+
+  if (hidden) {
+    return null;
+  }
+
+  const expanded = open ?? inner;
+  const panelId = `${autoId}-panel`;
+
+  const toggle = () => {
+    if (open === undefined) {
+      setInner(!expanded);
+    }
+    onOpenChange?.(!expanded);
+  };
+
+  return (
+    <div
+      className={cx(
+        "fox-accordion",
+        `fox-accordion--${type}`,
+        `fox-accordion--${size}`,
+        expanded && "fox-accordion--open",
+        className
+      )}
+    >
+      <button
+        className="fox-accordion__tit"
+        type="button"
+        aria-expanded={expanded}
+        aria-controls={panelId}
+        onClick={toggle}
+      >
+        {badge && <span className="fox-accordion__badge">{badge}</span>}
+        <span className="fox-accordion__title">{title}</span>
+        <span className="fox-accordion__icon" aria-hidden="true">
+          {expanded ? <FoxCaretUpIcon /> : <FoxCaretDownIcon />}
+        </span>
+      </button>
+
+      {/* 닫혔을 때는 DOM에서 뺀다 — 안의 초점 가능한 것들이 탭 순서에 남으면 안 된다. */}
+      {expanded && (
+        <div className="fox-accordion__contents" id={panelId}>
+          {children}
+        </div>
+      )}
+    </div>
+  );
+}
 
@fox/core/components/fox-accordion/index.ts (added)
+++ @fox/core/components/fox-accordion/index.ts
@@ -0,0 +1,6 @@
+export {
+  FoxAccordion,
+  type FoxAccordionProps,
+  type FoxAccordionType,
+  type FoxAccordionSize,
+} from "./fox-accordion";
 
@fox/core/components/fox-card/fox-card.tsx (added)
+++ @fox/core/components/fox-card/fox-card.tsx
@@ -0,0 +1,61 @@
+"use client";
+
+import type { ReactNode } from "react";
+import { cx } from "../../utils";
+
+export type FoxCardSize = "xlg" | "lg" | "md" | "sm";
+
+export interface FoxCardProps {
+  size?: FoxCardSize;
+  /** 위쪽 그림. 시안은 16:9다. */
+  image?: ReactNode;
+  title?: ReactNode;
+  /** 제목 아래 한두 줄 설명. */
+  description?: ReactNode;
+  /** 제목·설명 대신 넣을 것. 주면 `title`·`description`은 쓰이지 않는다. */
+  children?: ReactNode;
+  /** 아래쪽 줄. 시안은 글자 버튼이다. */
+  actions?: ReactNode;
+  /** 참이면 렌더하지 않는다(DOM에 남지 않는다). */
+  hidden?: boolean;
+  /** 배치 조정용. */
+  className?: string;
+}
+
+/**
+ * @fox 카드. 그림·글·동작이 위에서 아래로 쌓인다.
+ *
+ * 크기는 여백과 모서리만 바꾼다 — 폭은 놓이는 자리가 정한다.
+ *
+ * ⚠️ 스타일은 이 파일이 import하지 않는다 — 호스트 앱이 `@use "@fox/styles/components"`
+ * (또는 개별 파티셜)로 한 번 불러와야 한다.
+ */
+export function FoxCard({
+  size = "md",
+  image,
+  title,
+  description,
+  children,
+  actions,
+  hidden = false,
+  className,
+}: FoxCardProps) {
+  if (hidden) {
+    return null;
+  }
+
+  return (
+    <div className={cx("fox-card", `fox-card--${size}`, className)}>
+      {image && <div className="fox-card__image">{image}</div>}
+      <div className="fox-card__contents">
+        {children ?? (
+          <>
+            {title && <p className="fox-card__title">{title}</p>}
+            {description && <p className="fox-card__description">{description}</p>}
+          </>
+        )}
+      </div>
+      {actions && <div className="fox-card__actions">{actions}</div>}
+    </div>
+  );
+}
 
@fox/core/components/fox-card/index.ts (added)
+++ @fox/core/components/fox-card/index.ts
@@ -0,0 +1,1 @@
+export { FoxCard, type FoxCardProps, type FoxCardSize } from "./fox-card";
@fox/core/components/index.ts
--- @fox/core/components/index.ts
+++ @fox/core/components/index.ts
@@ -1,3 +1,4 @@
+export * from "./fox-accordion";
 export * from "./fox-address";
 export * from "./fox-badge";
 export * from "./fox-badge-dot";
@@ -8,6 +9,7 @@
 export * from "./fox-button";
 export * from "./fox-button-group";
 export * from "./fox-button-panel";
+export * from "./fox-card";
 export * from "./fox-carousel-area";
 export * from "./fox-carousel-button";
 export * from "./fox-carousel-indicator";
@fox/dev-test/component-registry.tsx
--- @fox/dev-test/component-registry.tsx
+++ @fox/dev-test/component-registry.tsx
@@ -58,6 +58,8 @@
 import { FoxToggleQuantity } from "../core/components/fox-toggle-quantity";
 import { FoxPageHeader } from "../core/components/fox-page-header";
 import { FoxModal } from "../core/components/fox-modal";
+import { FoxAccordion } from "../core/components/fox-accordion";
+import { FoxCard } from "../core/components/fox-card";
 import styles from "./dev-test.module.scss";
 
 export interface ComponentExample {
@@ -3382,4 +3384,91 @@
       },
     ],
   },
+  {
+    id: "fox-accordion",
+    name: "FoxAccordion",
+    description:
+      "Figma 시안(accordion 194:3563 box · 194:3680 line)을 옮겼습니다. box는 색을 깐 상자로 제목이 강조 색이고, line은 위쪽 선 하나로 나뉘는 목록형입니다. 열림을 밖에서 쥐면 여러 개 중 하나만 열리게 하는 것도 호출부가 정할 수 있고, 안 넘기면 스스로 여닫습니다. 닫히면 내용이 DOM에서 빠져 탭 순서에 남지 않습니다.",
+    variants: [
+      {
+        label: "box — lg · md",
+        node: (
+          <div className={styles.demoStack}>
+            <FoxAccordion size="lg" title="아코디언 타이틀 영역" defaultOpen
+              badge={<FoxBadgeGroup size="md"><FoxBadge size="md">레이블</FoxBadge></FoxBadgeGroup>}>
+              <p>펼쳐진 내용이 들어가는 자리입니다.</p>
+            </FoxAccordion>
+            <FoxAccordion size="md" title="아코디언 타이틀 영역"
+              badge={<FoxBadgeGroup size="sm"><FoxBadge size="sm">레이블</FoxBadge></FoxBadgeGroup>}>
+              <p>펼쳐진 내용이 들어가는 자리입니다.</p>
+            </FoxAccordion>
+          </div>
+        ),
+      },
+      {
+        label: "line — 이어 붙이면 위쪽 선이 칸막이가 됩니다",
+        node: (
+          <div className={styles.demoField}>
+            <FoxAccordion type="line" size="lg" title="아코디언 타이틀 영역" defaultOpen>
+              <p>펼쳐진 내용이 들어가는 자리입니다.</p>
+            </FoxAccordion>
+            <FoxAccordion type="line" size="lg" title="아코디언 타이틀 영역" />
+            <FoxAccordion type="line" size="md" title="아코디언 타이틀 영역" />
+          </div>
+        ),
+      },
+      {
+        label: "배지 없이",
+        node: (
+          <div className={styles.demoStack}>
+            <FoxAccordion title="아코디언 타이틀 영역">
+              <p>펼쳐진 내용이 들어가는 자리입니다.</p>
+            </FoxAccordion>
+          </div>
+        ),
+      },
+    ],
+  },
+  {
+    id: "fox-card",
+    name: "FoxCard",
+    description:
+      "Figma 시안(card 441:6006)을 옮겼습니다. 크기 넷은 여백과 모서리만 바꿉니다 — 폭은 놓이는 자리가 정합니다(시안은 320px 자리에 뒀습니다). 그림은 16:9로 잘리고 상자가 모서리를 넘지 않게 자릅니다.",
+    variants: [
+      {
+        label: "크기 — xlg · lg · md · sm (여백과 모서리가 달라집니다)",
+        node: (
+          <div className={styles.demoRow}>
+            {(["xlg", "lg", "md", "sm"] as const).map((size) => (
+              <div key={size} style={{ inlineSize: 320 }}>
+                <FoxCard
+                  size={size}
+                  title="타이틀 영역"
+                  description="간단한 설명이 들어가는 영역입니다."
+                  actions={<FoxTextButton size="md" label="액션1" />}
+                />
+              </div>
+            ))}
+          </div>
+        ),
+      },
+      {
+        label: "그림 없이 / 동작 없이 / 설명 없이",
+        node: (
+          <div className={styles.demoRow}>
+            <div style={{ inlineSize: 320 }}>
+              <FoxCard title="타이틀 영역" description="간단한 설명이 들어가는 영역입니다."
+                actions={<FoxTextButton size="md" label="액션1" />} />
+            </div>
+            <div style={{ inlineSize: 320 }}>
+              <FoxCard title="타이틀 영역" description="간단한 설명이 들어가는 영역입니다." />
+            </div>
+            <div style={{ inlineSize: 320 }}>
+              <FoxCard title="타이틀 영역" actions={<FoxTextButton size="md" label="액션1" />} />
+            </div>
+          </div>
+        ),
+      },
+    ],
+  },
 ];
 
@fox/styles/_fox-accordion.scss (added)
+++ @fox/styles/_fox-accordion.scss
@@ -0,0 +1,155 @@
+// FoxAccordion — 시안: 통합관리자페이지 디자인시스템(KE9UILWhB8qTBe14uXUn4F)
+//                      accordion (194:3563 box · 194:3680 line · 섹션 392:5034)
+//
+// 마크업 계약 (React 밖 소비자용):
+//   <div class="fox-accordion fox-accordion--box fox-accordion--lg fox-accordion--open">
+//     <button class="fox-accordion__tit" type="button" aria-expanded="true" aria-controls="…">
+//       <span class="fox-accordion__badge">…fox-badge-group…</span>   <!-- 선택 -->
+//       <span class="fox-accordion__title">제목</span>
+//       <span class="fox-accordion__icon">…svg…</span>
+//     </button>
+//     <div class="fox-accordion__contents" id="…">…</div>            <!-- 열렸을 때만 -->
+//   </div>
+//
+// 계열 둘이 성격이 다르다.
+//   - box:  색을 깐 상자. 여백·모서리를 상자가 갖고 제목이 강조 색이다.
+//   - line: 위쪽 선 하나로 나뉘는 목록형. 바탕이 없고 제목 줄이 세로 여백을 갖는다.
+//
+// 크기는 여백·글자·모서리를 함께 바꾼다(시안 lg·md).
+//
+// 토큰이 아닌 값:
+//   - line-height / letter-spacing: 시안 텍스트 스타일의 값이나 Figma가 변수로
+//     export하지 않는다.
+//   - 버튼 초기화(appearance/cursor/색 상속): 구조 규칙.
+
+@use "abstracts" as fox;
+
+.fox-accordion {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-start;
+  inline-size: 100%;
+
+  // 제목 줄은 통째로 누르는 자리다.
+  &__tit {
+    display: flex;
+    align-items: center;
+    gap: fox.gap(3);
+    inline-size: 100%;
+    margin: 0;
+    padding: 0;
+    border: none;
+    background: none;
+    font-family: fox.font-family(title);
+    font-weight: fox.font-weight(bold);
+    line-height: 1.4;
+    letter-spacing: -0.025em;
+    text-align: start;
+    cursor: pointer;
+  }
+
+  &__badge {
+    display: inline-flex;
+    flex-shrink: 0;
+  }
+
+  // 제목이 남는 폭을 먹고 아이콘은 오른쪽 끝에 붙는다.
+  &__title {
+    flex: 1 0 0;
+    min-inline-size: 0;
+    word-break: break-word;
+  }
+
+  &__icon {
+    display: inline-flex;
+    flex-shrink: 0;
+    color: fox.color(icon-neutral-default);
+
+    > * {
+      inline-size: fox.icon(4);
+      block-size: fox.icon(4);
+    }
+  }
+
+  &__contents {
+    display: flex;
+    flex-direction: column;
+    align-items: flex-start;
+    inline-size: 100%;
+    color: fox.color(font-neutral-default);
+    font-family: fox.font-family(body);
+    font-size: fox.font-size(body-md);
+    font-weight: fox.font-weight(regular);
+    line-height: 1.5;
+    letter-spacing: -0.025em;
+  }
+
+  // ── box 계열 ──────────────────────────────────────────────────────────────
+  &--box {
+    background: fox.color(surface-theme-secondary);
+  }
+
+  &--box &__title {
+    color: fox.color(font-theme-secondary);
+  }
+
+  &--box#{&}--lg {
+    gap: fox.gap(7);
+    padding-inline: fox.padding(9);
+    padding-block: fox.padding(8);
+    border-radius: fox.radius(5);
+  }
+
+  &--box#{&}--md {
+    gap: fox.gap(5);
+    padding-inline: fox.padding(7);
+    padding-block: fox.padding(6);
+    border-radius: fox.radius(4);
+  }
+
+  &--box#{&}--lg &__contents {
+    gap: fox.gap(7);
+  }
+
+  &--box#{&}--md &__contents {
+    gap: fox.gap(5);
+  }
+
+  // ── line 계열 ─────────────────────────────────────────────────────────────
+  // 여러 개를 이어 붙이면 위쪽 선이 칸막이가 된다.
+  &--line {
+    border-block-start: fox.border(1) solid fox.color(border-neutral-subtler);
+    background: fox.color(action-secondary);
+  }
+
+  &--line &__title {
+    color: fox.color(font-neutral-strong);
+  }
+
+  &--line#{&}--lg &__tit {
+    padding-block: fox.padding(8);
+  }
+
+  &--line#{&}--md &__tit {
+    padding-block: fox.padding(6);
+  }
+
+  &--line#{&}--lg &__contents {
+    gap: fox.gap(7);
+    padding-block-end: fox.padding(8);
+  }
+
+  &--line#{&}--md &__contents {
+    gap: fox.gap(5);
+    padding-block-end: fox.padding(6);
+  }
+
+  // ── 크기 (글자) ───────────────────────────────────────────────────────────
+  &--lg &__title {
+    font-size: fox.font-size(heading-sm);
+  }
+
+  &--md &__title {
+    font-size: fox.font-size(heading-xsm);
+  }
+}
 
@fox/styles/_fox-card.scss (added)
+++ @fox/styles/_fox-card.scss
@@ -0,0 +1,104 @@
+// FoxCard — 시안: 통합관리자페이지 디자인시스템(KE9UILWhB8qTBe14uXUn4F)
+//                  card (441:6006 · 섹션 463:5649)
+//
+// 마크업 계약 (React 밖 소비자용):
+//   <div class="fox-card fox-card--md">
+//     <div class="fox-card__image">…img…</div>          <!-- 선택 -->
+//     <div class="fox-card__contents">
+//       <p class="fox-card__title">타이틀</p>
+//       <p class="fox-card__description">설명</p>
+//     </div>
+//     <div class="fox-card__actions">…fox-text-button…</div>   <!-- 선택 -->
+//   </div>
+//
+// 크기 넷은 여백과 모서리만 바꾼다 — 폭은 놓이는 자리가 정한다(시안은 320px 자리에 뒀다).
+//
+// 그림은 16:9로 잘린다. 상자가 `overflow: clip`이라 위쪽 모서리를 그림이 넘지 않는다.
+//
+// 아래쪽 줄은 좌우·아래 여백만 갖는다 — 위쪽은 글 영역의 아래 여백이 이미 띄워 준다.
+//
+// 토큰이 아닌 값:
+//   - 그림 비율 16/9: 시안 값이나 Figma가 변수로 export하지 않는다(사용자 승인).
+//   - line-height / letter-spacing: 시안 텍스트 스타일의 값이나 Figma가 변수로
+//     export하지 않는다.
+//   - object-fit / overflow: 구조 규칙.
+
+@use "abstracts" as fox;
+
+.fox-card {
+  box-sizing: border-box;
+  display: flex;
+  flex-direction: column;
+  align-items: flex-start;
+  inline-size: 100%;
+  overflow: clip;
+  border: fox.border(1) solid fox.color(border-neutral-subtle);
+  background: fox.color(surface-neutral-default);
+
+  &__image {
+    inline-size: 100%;
+    // 시안 값이나 변수로 export되지 않는다 — 파일 상단 참고.
+    aspect-ratio: 16 / 9;
+
+    > * {
+      inline-size: 100%;
+      block-size: 100%;
+      object-fit: cover;
+    }
+  }
+
+  &__contents {
+    display: flex;
+    flex-direction: column;
+    align-items: flex-start;
+    gap: fox.gap(2);
+    inline-size: 100%;
+    word-break: break-word;
+  }
+
+  &__title {
+    inline-size: 100%;
+    margin: 0;
+    color: fox.color(font-neutral-strong);
+    font-family: fox.font-family(title);
+    font-size: fox.font-size(heading-sm);
+    font-weight: fox.font-weight(bold);
+    line-height: 1.4;
+    letter-spacing: -0.025em;
+  }
+
+  &__description {
+    inline-size: 100%;
+    margin: 0;
+    color: fox.color(font-neutral-default);
+    font-family: fox.font-family(body);
+    font-size: fox.font-size(body-md);
+    font-weight: fox.font-weight(regular);
+    line-height: 1.5;
+    letter-spacing: -0.025em;
+  }
+
+  // 위쪽은 글 영역의 아래 여백이 이미 띄워 준다.
+  &__actions {
+    display: flex;
+    align-items: flex-start;
+    gap: fox.gap(5);
+    inline-size: 100%;
+  }
+
+  // ── 크기 ──────────────────────────────────────────────────────────────────
+  @each $size in (xlg, lg, md, sm) {
+    &--#{$size} {
+      border-radius: fox.card(radius-#{$size});
+    }
+
+    &--#{$size} > .fox-card__contents {
+      padding: fox.card(padding-#{$size});
+    }
+
+    &--#{$size} > .fox-card__actions {
+      padding-inline: fox.card(padding-#{$size});
+      padding-block-end: fox.card(padding-#{$size});
+    }
+  }
+}
@fox/styles/components.scss
--- @fox/styles/components.scss
+++ @fox/styles/components.scss
@@ -53,3 +53,5 @@
 @use "fox-toggle-quantity";
 @use "fox-page-header";
 @use "fox-modal";
+@use "fox-accordion";
+@use "fox-card";
Add a comment
List