Repository: DaleStudy/daleui
Pull request #170: 링크 컴포넌트 구현
Author: login: hyoseong1994
Created: 2025-04-17T11:52:14Z | Merged: 2025-04-29T01:48:04Z

- Link 컴포넌트 구현
  - tone, size, weight, muted, underline 등의 props 지원
  - 보안을 위한 rel 속성 자동 추가 기능 구현

- 테스트 코드 작성
  - 기본 렌더링 테스트
  - tone 스타일 적용 테스트 (neutral, accent, danger, warning)
  - size와 weight 스타일 적용 테스트
  - muted 상태에 따른 스타일 테스트
  - underline 스타일 적용 테스트
  - 보안 속성(rel) 자동 추가 테스트
  - Icon 컴포넌트와의 통합 테스트

- Storybook 스토리 추가
  - 기본 사용법
  - 다양한 tone 예시
  - 명암비 예시
  - 밑줄 스타일 예시
  - size와 weight 예시
  - 보안 관련 예시




Diff:
diff --git a/src/components/Link/Link.stories.tsx b/src/components/Link/Link.stories.tsx
new file mode 100644
index 00000000..6802f03e
--- /dev/null
+++ b/src/components/Link/Link.stories.tsx
@@ -0,0 +1,192 @@
+import type { Meta, StoryObj } from "@storybook/react";
+import { vstack } from "../../../styled-system/patterns";
+import { Link } from "./Link";
+import { Icon } from "../Icon/Icon";
+
+export default {
+  component: Link,
+  parameters: {
+    layout: "centered",
+  },
+  args: {
+    children: "링크",
+    href: "#",
+  },
+} satisfies Meta<typeof Link>;
+
+export const Basic: StoryObj<typeof Link> = {};
+
+export const Tones: StoryObj<typeof Link> = {
+  render: (args) => {
+    return (
+      <div className={vstack({ gap: "gap.md" })}>
+        <Link {...args} tone="neutral">
+          중립 링크
+        </Link>
+        <Link {...args} tone="accent">
+          강조 링크
+        </Link>
+        <Link {...args} tone="danger">
+          위험 링크
+        </Link>
+        <Link {...args} tone="warning">
+          경고 링크
+        </Link>
+      </div>
+    );
+  },
+  argTypes: {
+    children: {
+      control: false,
+    },
+    tone: {
+      control: false,
+    },
+  },
+};
+
+export const Contrasts: StoryObj<typeof Link> = {
+  render: (args) => {
+    return (
+      <div className={vstack({ gap: "gap.md" })}>
+        <Link {...args} muted>
+          낮은 명암비
+        </Link>
+        <Link {...args}>높은 명암비</Link>
+      </div>
+    );
+  },
+  argTypes: {
+    children: {
+      control: false,
+    },
+    muted: {
+      control: false,
+    },
+  },
+};
+
+export const Underlines: StoryObj<typeof Link> = {
+  render: (args) => {
+    return (
+      <div className={vstack({ gap: "gap.md" })}>
+        <Link {...args} underline>
+          밑줄 있음
+        </Link>
+        <Link {...args} underline={false}>
+          밑줄 없음
+        </Link>
+      </div>
+    );
+  },
+  argTypes: {
+    children: {
+      control: false,
+    },
+    underline: {
+      control: false,
+    },
+  },
+};
+
+export const Sizes: StoryObj<typeof Link> = {
+  render: (args) => {
+    return (
+      <div className={vstack({ gap: "gap.md" })}>
+        <Link {...args} size="sm">
+          작은 링크
+        </Link>
+        <Link {...args} size="md">
+          중간 링크
+        </Link>
+        <Link {...args} size="lg">
+          큰 링크
+        </Link>
+      </div>
+    );
+  },
+  argTypes: {
+    children: {
+      control: false,
+    },
+    size: {
+      control: false,
+    },
+  },
+};
+
+export const Weights: StoryObj<typeof Link> = {
+  render: (args) => {
+    return (
+      <div className={vstack({ gap: "gap.md" })}>
+        <Link {...args} weight="thin">
+          얇은 굵기
+        </Link>
+        <Link {...args} weight="light">
+          가벼운 굵기
+        </Link>
+        <Link {...args} weight="normal">
+          일반 굵기
+        </Link>
+        <Link {...args} weight="medium">
+          중간 굵기
+        </Link>
+        <Link {...args} weight="bold">
+          굵은 굵기
+        </Link>
+      </div>
+    );
+  },
+  argTypes: {
+    children: {
+      control: false,
+    },
+    weight: {
+      control: false,
+    },
+  },
+};
+
+export const Security: StoryObj<typeof Link> = {
+  render: (args) => {
+    return (
+      <div className={vstack({ gap: "gap.md" })}>
+        <Link {...args} target="_blank">
+          새 탭에서 열기 (보안 속성 자동 추가)
+        </Link>
+        <Link {...args} target="_self">
+          같은 탭에서 열기
+        </Link>
+      </div>
+    );
+  },
+  argTypes: {
+    children: {
+      control: false,
+    },
+    target: {
+      control: false,
+    },
+  },
+};
+
+export const WithIcon: StoryObj<typeof Link> = {
+  render: (args) => {
+    return (
+      <Link {...args} target="_blank">
+        <Icon name="chevronRight" />
+        링크 아이콘
+      </Link>
+    );
+  },
+};
+
+export const Inline: StoryObj<typeof Link> = {
+  render: (args) => {
+    return (
+      <p>
+        이 문장에는 <Link {...args}>링크</Link>가 포함되어 있습니다.
+      </p>
+    );
+  },
+};
diff --git a/src/components/Link/Link.test.tsx b/src/components/Link/Link.test.tsx
index 8576f725..f3ef5162 100644
--- a/src/components/Link/Link.test.tsx
+++ b/src/components/Link/Link.test.tsx
@@ -1,19 +1,151 @@
-import { test } from "vitest";
+import { faker } from "@faker-js/faker";
+import { composeStories } from "@storybook/react";
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { describe, expect, test, vi } from "vitest";
+import { fontSizes, fontWeights } from "../../tokens/typography";
+import * as stories from "./Link.stories";
 
-test.todo("renders children correctly");
+const { Basic, Tones, Contrasts, Underlines, Security, WithIcon } =
+  composeStories(stories);
 
-test.todo("applies the correct tone based on the 'tone' prop");
+describe("rendering test", () => {
+  test("renders the link with the correct text content", () => {
+    render(<Basic />);
+    expect(screen.getByRole("link")).toHaveTextContent("링크");
+  });
 
-test.todo("applies the correct font size based on the 'size' prop");
+  test("can be used with Icon component", () => {
+    render(<WithIcon />);
 
-test.todo("applies the correct font weight based on the 'weight' prop");
+    const link = screen.getByRole("link");
+    expect(link).toHaveTextContent("링크");
+    expect(link.querySelector("svg")).toBeInTheDocument();
+  });
+});
 
-test.todo("renders with reduced contrast when 'muted' is true");
+describe("style test", () => {
+  test("applies the correct tone based on the 'tone' prop", () => {
+    render(<Tones />);
 
-test.todo("renders with underline when 'underline' is true");
+    expect(screen.getByRole("link", { name: "중립 링크" })).toHaveClass(
+      "c_text"
+    );
+    expect(screen.getByRole("link", { name: "강조 링크" })).toHaveClass(
+      "c_text.accent"
+    );
+    expect(screen.getByRole("link", { name: "위험 링크" })).toHaveClass(
+      "c_text.danger"
+    );
+    expect(screen.getByRole("link", { name: "경고 링크" })).toHaveClass(
+      "c_text.warning"
+    );
+  });
 
-test.todo("forwards additional anchor props like 'href' and 'target'");
+  test("applies the correct font size based on the 'size' prop", () => {
+    const size = faker.helpers.arrayElement(
+      Object.keys(fontSizes)
+    ) as keyof typeof fontSizes;
 
-test.todo("adds rel='noopener noreferrer' when target is '_blank'");
+    render(<Basic size={size} />);
+    expect(screen.getByRole("link")).toHaveStyle({ fontSize: size });
+  });
 
-test.todo("can be used with Icon component");
+  test("applies the correct font weight based on the 'weight' prop", () => {
+    const weight = faker.helpers.arrayElement(
+      Object.keys(fontWeights)
+    ) as keyof typeof fontWeights;
+
+    render(<Basic weight={weight} />);
+    expect(screen.getByRole("link")).toHaveClass(`fw_${weight}`);
+  });
+
+  test("applies the correct styles for low and high contrast", () => {
+    render(<Contrasts />);
+
+    expect(screen.getByRole("link", { name: "낮은 명암비" })).toHaveClass(
+      "c_text.muted"
+    );
+
+    expect(screen.getByRole("link", { name: "높은 명암비" })).toHaveClass(
+      "c_text"
+    );
+  });
+
+  test("applies the correct underline styles", () => {
+    render(<Underlines />);
+
+    expect(screen.getByRole("link", { name: "밑줄 있음" })).toHaveClass(
+      "td_underline"
+    );
+    expect(screen.getByRole("link", { name: "밑줄 없음" })).toHaveClass(
+      "td_none"
+    );
+  });
+});
+
+describe("behavior test", () => {
+  test("forwards additional anchor props like 'href' and 'target'", () => {
+    const href = faker.internet.url({ appendSlash: true });
+    const target = "_self";
+
+    render(<Basic href={href} target={target} />);
+
+    const link = screen.getByRole("link");
+    expect(link).toHaveAttribute("href", href);
+    expect(link).toHaveAttribute("target", target);
+  });
+
+  test("adds rel='noopener noreferrer' when target is '_blank'", () => {
+    render(<Security />);
+
+    expect(
+      screen.getByRole("link", { name: "새 탭에서 열기 (보안 속성 자동 추가)" })
+    ).toHaveAttribute("rel", "noopener noreferrer");
+
+    expect(
+      screen.getByRole("link", { name: "같은 탭에서 열기" })
+    ).not.toHaveAttribute("rel", "noopener noreferrer");
+  });
+
+  test("navigates to the correct URL when clicked", async () => {
+    const href = faker.internet.url({ appendSlash: true });
+    render(<Basic href={href} />);
+
+    const link = screen.getByRole("link");
+    expect(link).toHaveAttribute("href", href);
+    await userEvent.click(link);
+
+    expect(window.location.href).toBe(href);
+  });
+
+  test("handles click events correctly", async () => {
+    const handleClick = vi.fn();
+    render(<Basic href="#" onClick={handleClick} />);
+
+    const link = screen.getByRole("link");
+    await userEvent.click(link);
+
+    expect(handleClick).toHaveBeenCalledTimes(1);
+  });
+
+  test("link is keyboard focusable", async () => {
+    const href = faker.internet.url({ appendSlash: true });
+    render(<Basic href={href} />);
+
+    await userEvent.tab();
+    const link = screen.getByRole("link");
+
+    expect(link).toHaveFocus();
+  });
+
+  test("navigates when link is clicked with keyboard", async () => {
+    const href = faker.internet.url({ appendSlash: true });
+    render(<Basic href={href} />);
+
+    await userEvent.tab();
+    await userEvent.keyboard("{Enter}");
+
+    expect(window.location.href).toBe(href);
+  });
+});
diff --git a/src/components/Link/Link.tsx b/src/components/Link/Link.tsx
index 182dd8cd..e0277b04 100644
--- a/src/components/Link/Link.tsx
+++ b/src/components/Link/Link.tsx
@@ -1,5 +1,6 @@
-import type { AnchorHTMLAttributes, PropsWithChildren } from "react";
+import { type AnchorHTMLAttributes, type PropsWithChildren } from "react";
 import type { Tone } from "../../tokens/colors";
+import { css, cva } from "../../../styled-system/css";
 import type { FontSize, FontWeight } from "../../tokens/typography";
 
 interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
@@ -15,6 +16,159 @@ interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
   underline?: boolean;
 }
 
-export function Link({ children }: PropsWithChildren<LinkProps>) {
-  return <>{children}</>;
+/**
+ * - `underline` 속성으로 밑줄 표시 여부를 설정할 수 있습니다. 기본값은 `true`입니다.
+ * - `target="_blank"`를 사용할 때는 자동으로 보안 속성이 추가됩니다.
+ * - `tone` 속성을 통해서 링크의 색상을 변경할 수 있습니다. 기본값은 `neutral`입니다.
+ * - `size` 속성과 `weight` 속성을 통해서 텍스트 스타일을 변경할 수 있습니다.
+ * - `muted` 속성을 주시면 글자색이 옅어집니다. 명암비가 낮아지므로 접근성 측면에서 주의해서 사용하세요. 기본값은 `false`입니다.
+ */
+export function Link({
+  children,
+  tone = "neutral",
+  size,
+  weight,
+  muted = false,
+  underline = true,
+  target,
+  rel,
+  ...props
+}: PropsWithChildren<LinkProps>) {
+  return (
+    <a
+      className={css(
+        styles.raw({ tone, muted, underline }),
+        css.raw({
+          fontSize: size,
+          fontWeight: weight,
+        })
+      )}
+      target={target}
+      rel={target === "_blank" ? "noopener noreferrer" : rel}
+      {...props}
+    >
+      {children}
+    </a>
+  );
 }
+
+const styles = cva({
+  base: {
+    transition: "colors 0.2s",
+  },
+  variants: {
+    tone: {
+      neutral: {},
+      accent: {},
+      danger: {},
+      warning: {},
+    },
+    muted: {
+      true: {},
+      false: {},
+    },
+    underline: {
+      true: {},
+      false: {},
+    },
+  },
+  compoundVariants: [
+    {
+      tone: "neutral",
+      muted: false,
+      css: {
+        color: "text",
+        "&:active, &:hover": {
+          color: "text.muted",
+        },
+      },
+    },
+    {
+      tone: "accent",
+      muted: false,
+      css: {
+        color: "text.accent",
+        "&:active, &:hover": {
+          color: "text.muted.accent",
+        },
+      },
+    },
+    {
+      tone: "danger",
+      muted: false,
+      css: {
+        color: "text.danger",
+        "&:active, &:hover": {
+          color: "text.muted.danger",
+        },
+      },
+    },
+    {
+      tone: "warning",
+      muted: false,
+      css: {
+        color: "text.warning",
+        "&:active, &:hover": {
+          color: "text.muted.warning",
+        },
+      },
+    },
+    {
+      tone: "neutral",
+      muted: true,
+      css: {
+        color: "text.muted",
+        "&:active, &:hover": {
+          color: "text",
+        },
+      },
+    },
+    {
+      tone: "accent",
+      muted: true,
+      css: {
+        color: "text.muted.accent",
+        "&:active, &:hover": {
+          color: "text.accent",
+        },
+      },
+    },
+    {
+      tone: "danger",
+      muted: true,
+      css: {
+        color: "text.muted.danger",
+        "&:active, &:hover": {
+          color: "text.danger",
+        },
+      },
+    },
+    {
+      tone: "warning",
+      muted: true,
+      css: {
+        color: "text.muted.warning",
+        "&:active, &:hover": {
+          color: "text.warning",
+        },
+      },
+    },
+    {
+      underline: true,
+      css: {
+        textDecoration: "underline",
+      },
+    },
+    {
+      underline: false,
+      css: {
+        textDecoration: "none",
+      },
+    },
+  ],
+  defaultVariants: {
+    tone: "neutral",
+    muted: false,
+    underline: true,
+  },
+});


Reviews:
user: login: DaleSeo
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-18T00:00:24Z
user: login: hyoseong1994
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T00:06:46Z
user: login: hyoseong1994
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T00:08:18Z
user: login: DaleSeo
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-18T03:06:46Z
user: login: DaleSeo
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-18T03:11:34Z
user: login: Sunjae95
body: 저는 별도의 이견은 없습니다.
달레님 코멘트 부분 반영되면 다시 확인해볼게요!
LGTM! 🙂 
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-21T04:39:20Z
user: login: hyoseong1994
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-21T06:17:04Z
user: login: hyoseong1994
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-21T06:17:19Z
user: login: hyoseong1994
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-21T06:17:33Z
user: login: hyoseong1994
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-21T06:19:07Z
user: login: hyoseong1994
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-21T06:21:30Z
user: login: hyoseong1994
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-21T06:22:15Z
user: login: hyoseong1994
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-21T06:23:50Z
user: login: DaleSeo
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-22T00:49:24Z
user: login: DaleSeo
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-22T00:50:48Z
user: login: hyoseong1994
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T01:24:30Z
user: login: sounmind
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-22T04:05:04Z
user: login: hyoseong1994
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T05:48:54Z
user: login: hyoseong1994
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T05:54:30Z
user: login: hyoseong1994
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T15:21:03Z
user: login: DaleSeo
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-23T23:49:00Z
user: login: DaleSeo
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-23T23:54:10Z
user: login: DaleSeo
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-24T00:03:06Z
user: login: Sunjae95
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-24T00:10:42Z
user: login: hyoseong1994
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T00:31:03Z
user: login: hyoseong1994
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T00:35:11Z
user: login: hyoseong1994
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T00:37:46Z
user: login: hyoseong1994
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T00:41:24Z
user: login: hyoseong1994
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T00:50:10Z
user: login: DaleSeo
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-24T00:58:48Z
user: login: DaleSeo
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-24T01:05:39Z
user: login: hyoseong1994
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T01:08:10Z
user: login: DaleSeo
body: Mismatch 이슈는 별도 티켓에서 해결하기로 했으니 이 정도면 준수한 1차 구현이 나온 것 같습니다. 수고 많으셨습니다! 🙇
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-25T22:21:46Z
user: login: DaleSeo
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-25T22:25:21Z
user: login: hyoseong1994
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T22:40:24Z
user: login: hyoseong1994
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T22:41:38Z
user: login: sounmind
body: 고생하셨어요!!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-29T00:39:01Z
user: login: Sunjae95
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-29T01:38:09Z
user: login: Sunjae95
body: 고생하셨습니다!🙂
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-29T01:38:59Z
user: login: RiaOh
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-03T08:49:06Z

Review comments:
diff_hunk: @@ -15,6 +16,159 @@ interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
   underline?: boolean;
 }
 
-export function Link({ children }: PropsWithChildren<LinkProps>) {
-  return <>{children}</>;
+/**
+ * - `underline` 속성으로 밑줄 표시 여부를 설정할 수 있습니다.
+ * - `target="_blank"`를 사용할 때는 자동으로 보안 속성이 추가됩니다.
+ * - `tone` 속성을 통해서 링크의 색상을 변경할 수 있습니다.
+ * - `size` 속성과 `weight` 속성을 통해서 텍스트 스타일을 변경할 수 있습니다.
+ * - `muted` 속성을 주시면 글자색이 옅어집니다. 명암비가 낮아지므로 접근성 측면에서 주의해서 사용하세요.
+ */
path: src/components/Link/Link.tsx
user: login: DaleSeo
body: 컴포넌트에 대한 설명이 친절하고 좋네요! 👍👍
나중에 디자이너분들께서 제공해 줄 정식 가이드로 바뀔 때까지 좋은 Placeholder가 될 것 같습니다. 😸
created_at: 2025-04-17T23:56:13Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,170 @@
+import type { Meta, StoryObj } from "@storybook/react";
+import { vstack } from "../../../styled-system/patterns";
+import { Link } from "./Link";
+
+export default {
+  component: Link,
+  parameters: {
+    layout: "centered",
+  },
+  args: {
+    children: "링크",
+    href: "https://www.daleui.com",
path: src/components/Link/Link.stories.tsx
user: login: DaleSeo
body: `href` 속성의 기본값을 `"#"`로 하면 어떨까요? 스토리북에서 링크를 클릭했을 때, 다른 페이지로 이동하는 게 좋지 않은 경험이 될 수도 있을 것 같아서요. 어떻게 생각하시나요?
created_at: 2025-04-18T00:00:21Z
author_association: MEMBER
diff_hunk: @@ -15,6 +16,159 @@ interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
   underline?: boolean;
 }
 
-export function Link({ children }: PropsWithChildren<LinkProps>) {
-  return <>{children}</>;
+/**
+ * - `underline` 속성으로 밑줄 표시 여부를 설정할 수 있습니다.
+ * - `target="_blank"`를 사용할 때는 자동으로 보안 속성이 추가됩니다.
+ * - `tone` 속성을 통해서 링크의 색상을 변경할 수 있습니다.
+ * - `size` 속성과 `weight` 속성을 통해서 텍스트 스타일을 변경할 수 있습니다.
+ * - `muted` 속성을 주시면 글자색이 옅어집니다. 명암비가 낮아지므로 접근성 측면에서 주의해서 사용하세요.
+ */
path: src/components/Link/Link.tsx
user: login: hyoseong1994
body: 기존에 작성되어있는 컴포넌트들을 참고해서 작성했습니다. 감사합니다. :)
created_at: 2025-04-18T00:06:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,170 @@
+import type { Meta, StoryObj } from "@storybook/react";
+import { vstack } from "../../../styled-system/patterns";
+import { Link } from "./Link";
+
+export default {
+  component: Link,
+  parameters: {
+    layout: "centered",
+  },
+  args: {
+    children: "링크",
+    href: "https://www.daleui.com",
path: src/components/Link/Link.stories.tsx
user: login: hyoseong1994
body: 좋은 생각입니다. 안그래도 어떤 링크를 넣는게 좋을까 하다가 daleui로 넣었는데 `#`으로 막으면 스토리북 보는중에 이동도 없어질것이고 control을 통해서 수정하여 페이지 이동도 할 수 있겠네요 오늘도 하나 배워갑니다. 감사합니다.
created_at: 2025-04-18T00:08:18Z
author_association: CONTRIBUTOR
path: src/components/Link/Link.stories.tsx
user: login: DaleSeo
body: https://github.com/DaleStudy/daleui/issues/35#issuecomment-2761147411 에서 언급해주신 것처럼 Icon 링크와 inline 링크 스토리도 추가해주실 수 있으실까요?

![Shot 2025-04-17 at 22 39 49](https://github.com/user-attachments/assets/aacfc235-fa27-427a-bd3f-0343807fee37)

created_at: 2025-04-18T02:41:29Z
author_association: MEMBER
diff_hunk: @@ -15,6 +16,159 @@ interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
   underline?: boolean;
 }
 
-export function Link({ children }: PropsWithChildren<LinkProps>) {
-  return <>{children}</>;
+/**
+ * - `underline` 속성으로 밑줄 표시 여부를 설정할 수 있습니다.
+ * - `target="_blank"`를 사용할 때는 자동으로 보안 속성이 추가됩니다.
+ * - `tone` 속성을 통해서 링크의 색상을 변경할 수 있습니다.
+ * - `size` 속성과 `weight` 속성을 통해서 텍스트 스타일을 변경할 수 있습니다.
+ * - `muted` 속성을 주시면 글자색이 옅어집니다. 명암비가 낮아지므로 접근성 측면에서 주의해서 사용하세요.
+ */
+export function Link({
+  children,
+  tone,
+  size,
+  weight,
+  muted,
+  underline,
path: src/components/Link/Link.tsx
user: login: DaleSeo
body: 기본값을 명시해줘야 하지 않을까요?
created_at: 2025-04-18T02:46:30Z
author_association: MEMBER
diff_hunk: @@ -1,19 +1,111 @@
-import { test } from "vitest";
+import { faker } from "@faker-js/faker";
+import { composeStories } from "@storybook/react";
+import { render, screen } from "@testing-library/react";
+import { expect, test } from "vitest";
+import { fontSizes, fontWeights } from "../../tokens/typography";
+import * as stories from "./Link.stories";
+import { Icon } from "../Icon/Icon";
 
-test.todo("renders children correctly");
+const { Basic, Tones, Contrasts, Underlines, Security } =
+  composeStories(stories);
 
-test.todo("applies the correct tone based on the 'tone' prop");
+test("renders the link with the correct text content", () => {
+  render(<Basic>링크</Basic>);
+  expect(screen.getByRole("link")).toHaveTextContent("링크");
+});
 
-test.todo("applies the correct font size based on the 'size' prop");
+test("applies the correct tone based on the 'tone' prop", () => {
+  render(<Tones />);
 
-test.todo("applies the correct font weight based on the 'weight' prop");
+  expect(screen.getByRole("link", { name: "중립 링크" })).toHaveClass("c_text");
+  expect(screen.getByRole("link", { name: "강조 링크" })).toHaveClass(
+    "c_text.accent"
+  );
+  expect(screen.getByRole("link", { name: "위험 링크" })).toHaveClass(
+    "c_text.danger"
+  );
+  expect(screen.getByRole("link", { name: "경고 링크" })).toHaveClass(
+    "c_text.warning"
+  );
+});
 
-test.todo("renders with reduced contrast when 'muted' is true");
+test("applies the correct font size based on the 'size' prop", () => {
+  const size = faker.helpers.arrayElement(
+    Object.keys(fontSizes)
+  ) as keyof typeof fontSizes;
 
-test.todo("renders with underline when 'underline' is true");
+  render(<Basic size={size} />);
+  expect(screen.getByRole("link")).toHaveStyle({ fontSize: size });
+});
 
-test.todo("forwards additional anchor props like 'href' and 'target'");
+test("applies the correct font weight based on the 'weight' prop", () => {
+  const weight = faker.helpers.arrayElement(
+    Object.keys(fontWeights)
+  ) as keyof typeof fontWeights;
 
-test.todo("adds rel='noopener noreferrer' when target is '_blank'");
+  render(<Basic weight={weight} />);
+  expect(screen.getByRole("link")).toHaveClass(`fw_${weight}`);
+});
 
-test.todo("can be used with Icon component");
+test("applies the correct styles for low and high contrast", () => {
+  render(<Contrasts />);
+
+  expect(screen.getByRole("link", { name: "낮은 명암비" })).toHaveClass(
+    "c_text.muted"
+  );
+
+  expect(screen.getByRole("link", { name: "높은 명암비" })).toHaveClass(
+    "c_text"
+  );
+});
+
+test("applies the correct underline styles", () => {
+  render(<Underlines />);
+
+  expect(screen.getByRole("link", { name: "밑줄 있음" })).toHaveClass(
+    "td_underline"
+  );
+  expect(screen.getByRole("link", { name: "밑줄 없음" })).toHaveClass(
+    "td_none"
+  );
+});
+
+test("forwards additional anchor props like 'href' and 'target'", () => {
+  const href = "https://www.daleui.com";
path: src/components/Link/Link.test.tsx
user: login: DaleSeo
body: 이 테스트에서 `href`가 어떤 값인지는 중요하지 않으니 Faker를 써도 좋을 것 같습니다.

```suggestion
  const href = faker.internet.url();
```
created_at: 2025-04-18T02:51:08Z
author_association: MEMBER
path: src/components/Link/Link.test.tsx
user: login: DaleSeo
body: 링크에 대한 시각적인 부분만 테스트가 되어 있네요. 사용자와 상호작용하는 컴포넌트인 만큼 동작적인 부분도 테스트가 되야할 것 같습니다.
- 마우스로 클릭했을 때, 페이지가 이동함
- 키보드의 탭 키로 선택 후에 엔티키를 눌렀을 때, 페이지가 이동함
created_at: 2025-04-18T03:03:11Z
author_association: MEMBER
diff_hunk: @@ -15,6 +16,159 @@ interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
   underline?: boolean;
 }
 
-export function Link({ children }: PropsWithChildren<LinkProps>) {
-  return <>{children}</>;
+/**
+ * - `underline` 속성으로 밑줄 표시 여부를 설정할 수 있습니다.
+ * - `target="_blank"`를 사용할 때는 자동으로 보안 속성이 추가됩니다.
+ * - `tone` 속성을 통해서 링크의 색상을 변경할 수 있습니다.
+ * - `size` 속성과 `weight` 속성을 통해서 텍스트 스타일을 변경할 수 있습니다.
+ * - `muted` 속성을 주시면 글자색이 옅어집니다. 명암비가 낮아지므로 접근성 측면에서 주의해서 사용하세요.
+ */
+export function Link({
+  children,
+  tone,
+  size,
+  weight,
+  muted,
+  underline,
+  target,
+  rel,
+  ...props
+}: PropsWithChildren<LinkProps>) {
+  return (
+    <a
+      className={css(
+        styles.raw({ tone, muted, underline }),
+        css.raw({
+          fontSize: size,
+          fontWeight: weight,
+        })
+      )}
+      target={target}
+      rel={target === "_blank" ? "noopener noreferrer" : rel}
+      {...props}
+    >
+      {children}
+    </a>
+  );
 }
+
+const styles = cva({
+  base: {
+    transition: "colors 0.2s",
+  },
+  variants: {
+    tone: {
+      neutral: {},
+      accent: {},
+      danger: {},
+      warning: {},
+    },
+    muted: {
+      true: {},
+      false: {},
+    },
+    underline: {
+      true: {},
+      false: {},
+    },
+  },
+  compoundVariants: [
+    {
+      tone: "neutral",
+      muted: false,
+      css: {
+        color: "text",
+        "&:active, &:hover": {
+          color: "text.muted",
+        },
+      },
+    },
+    {
+      tone: "accent",
+      muted: false,
+      css: {
+        color: "text.accent",
path: src/components/Link/Link.tsx
user: login: DaleSeo
body: ```tsx
      <Link tone="danger">
        <Text tone="warning">
          나의 밑줄은 무슨 색일까? 🤔
        </Text>
      </Link>
```
created_at: 2025-04-18T03:05:36Z
author_association: MEMBER
diff_hunk: @@ -15,6 +16,159 @@ interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
   underline?: boolean;
 }
 
-export function Link({ children }: PropsWithChildren<LinkProps>) {
-  return <>{children}</>;
+/**
+ * - `underline` 속성으로 밑줄 표시 여부를 설정할 수 있습니다.
+ * - `target="_blank"`를 사용할 때는 자동으로 보안 속성이 추가됩니다.
+ * - `tone` 속성을 통해서 링크의 색상을 변경할 수 있습니다.
+ * - `size` 속성과 `weight` 속성을 통해서 텍스트 스타일을 변경할 수 있습니다.
+ * - `muted` 속성을 주시면 글자색이 옅어집니다. 명암비가 낮아지므로 접근성 측면에서 주의해서 사용하세요.
+ */
+export function Link({
+  children,
+  tone,
+  size,
+  weight,
+  muted,
+  underline,
+  target,
+  rel,
+  ...props
+}: PropsWithChildren<LinkProps>) {
+  return (
+    <a
+      className={css(
+        styles.raw({ tone, muted, underline }),
+        css.raw({
+          fontSize: size,
path: src/components/Link/Link.tsx
user: login: DaleSeo
body: 이 아이콘 링크는 클까요? 작을까요? 🤔

```tsx
      <Link size="xl" {...args} href="https://www.daleui.com">
        <Icon name="clock" size="xs" />
      </Link>
```
created_at: 2025-04-18T03:11:34Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,170 @@
+import type { Meta, StoryObj } from "@storybook/react";
+import { vstack } from "../../../styled-system/patterns";
+import { Link } from "./Link";
+
+export default {
+  component: Link,
+  parameters: {
+    layout: "centered",
+  },
+  args: {
+    children: "링크",
+    href: "https://www.daleui.com",
path: src/components/Link/Link.stories.tsx
user: login: hyoseong1994
body: href `"#"` 속성 추가 및 onclick `(e: React.MouseEvent) => e.preventDefault()` 를 추가하여 화면이동 안하도록 수정했습니다.
created_at: 2025-04-21T06:17:04Z
author_association: CONTRIBUTOR
path: src/components/Link/Link.stories.tsx
user: login: hyoseong1994
body: 해당 내용 스토리 추가했습니다.
created_at: 2025-04-21T06:17:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -15,6 +16,159 @@ interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
   underline?: boolean;
 }
 
-export function Link({ children }: PropsWithChildren<LinkProps>) {
-  return <>{children}</>;
+/**
+ * - `underline` 속성으로 밑줄 표시 여부를 설정할 수 있습니다.
+ * - `target="_blank"`를 사용할 때는 자동으로 보안 속성이 추가됩니다.
+ * - `tone` 속성을 통해서 링크의 색상을 변경할 수 있습니다.
+ * - `size` 속성과 `weight` 속성을 통해서 텍스트 스타일을 변경할 수 있습니다.
+ * - `muted` 속성을 주시면 글자색이 옅어집니다. 명암비가 낮아지므로 접근성 측면에서 주의해서 사용하세요.
+ */
+export function Link({
+  children,
+  tone,
+  size,
+  weight,
+  muted,
+  underline,
path: src/components/Link/Link.tsx
user: login: hyoseong1994
body: 기본값 명시 추가했습니다. (사용자가 보기에 편하게 하는게 좋을거 같아서 jsdoc 에도 기본값 내용 추가했습니다.)
created_at: 2025-04-21T06:17:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,19 +1,111 @@
-import { test } from "vitest";
+import { faker } from "@faker-js/faker";
+import { composeStories } from "@storybook/react";
+import { render, screen } from "@testing-library/react";
+import { expect, test } from "vitest";
+import { fontSizes, fontWeights } from "../../tokens/typography";
+import * as stories from "./Link.stories";
+import { Icon } from "../Icon/Icon";
 
-test.todo("renders children correctly");
+const { Basic, Tones, Contrasts, Underlines, Security } =
+  composeStories(stories);
 
-test.todo("applies the correct tone based on the 'tone' prop");
+test("renders the link with the correct text content", () => {
+  render(<Basic>링크</Basic>);
+  expect(screen.getByRole("link")).toHaveTextContent("링크");
+});
 
-test.todo("applies the correct font size based on the 'size' prop");
+test("applies the correct tone based on the 'tone' prop", () => {
+  render(<Tones />);
 
-test.todo("applies the correct font weight based on the 'weight' prop");
+  expect(screen.getByRole("link", { name: "중립 링크" })).toHaveClass("c_text");
+  expect(screen.getByRole("link", { name: "강조 링크" })).toHaveClass(
+    "c_text.accent"
+  );
+  expect(screen.getByRole("link", { name: "위험 링크" })).toHaveClass(
+    "c_text.danger"
+  );
+  expect(screen.getByRole("link", { name: "경고 링크" })).toHaveClass(
+    "c_text.warning"
+  );
+});
 
-test.todo("renders with reduced contrast when 'muted' is true");
+test("applies the correct font size based on the 'size' prop", () => {
+  const size = faker.helpers.arrayElement(
+    Object.keys(fontSizes)
+  ) as keyof typeof fontSizes;
 
-test.todo("renders with underline when 'underline' is true");
+  render(<Basic size={size} />);
+  expect(screen.getByRole("link")).toHaveStyle({ fontSize: size });
+});
 
-test.todo("forwards additional anchor props like 'href' and 'target'");
+test("applies the correct font weight based on the 'weight' prop", () => {
+  const weight = faker.helpers.arrayElement(
+    Object.keys(fontWeights)
+  ) as keyof typeof fontWeights;
 
-test.todo("adds rel='noopener noreferrer' when target is '_blank'");
+  render(<Basic weight={weight} />);
+  expect(screen.getByRole("link")).toHaveClass(`fw_${weight}`);
+});
 
-test.todo("can be used with Icon component");
+test("applies the correct styles for low and high contrast", () => {
+  render(<Contrasts />);
+
+  expect(screen.getByRole("link", { name: "낮은 명암비" })).toHaveClass(
+    "c_text.muted"
+  );
+
+  expect(screen.getByRole("link", { name: "높은 명암비" })).toHaveClass(
+    "c_text"
+  );
+});
+
+test("applies the correct underline styles", () => {
+  render(<Underlines />);
+
+  expect(screen.getByRole("link", { name: "밑줄 있음" })).toHaveClass(
+    "td_underline"
+  );
+  expect(screen.getByRole("link", { name: "밑줄 없음" })).toHaveClass(
+    "td_none"
+  );
+});
+
+test("forwards additional anchor props like 'href' and 'target'", () => {
+  const href = "https://www.daleui.com";
path: src/components/Link/Link.test.tsx
user: login: hyoseong1994
body: faker로 변경했습니다.
created_at: 2025-04-21T06:19:07Z
author_association: CONTRIBUTOR
path: src/components/Link/Link.test.tsx
user: login: hyoseong1994
body: 하위 테스트 항목 추가했습니다.
- 클릭시 페이지 이동 테스트(href)
- 클릭시 onClick 호출 테스트
- 키보드 접근성 테스트 (포커스)
- 키보드 엔터 테스트(href)
- 키보드 엔터 테스트(onClick)
created_at: 2025-04-21T06:21:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -15,6 +16,159 @@ interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
   underline?: boolean;
 }
 
-export function Link({ children }: PropsWithChildren<LinkProps>) {
-  return <>{children}</>;
+/**
+ * - `underline` 속성으로 밑줄 표시 여부를 설정할 수 있습니다.
+ * - `target="_blank"`를 사용할 때는 자동으로 보안 속성이 추가됩니다.
+ * - `tone` 속성을 통해서 링크의 색상을 변경할 수 있습니다.
+ * - `size` 속성과 `weight` 속성을 통해서 텍스트 스타일을 변경할 수 있습니다.
+ * - `muted` 속성을 주시면 글자색이 옅어집니다. 명암비가 낮아지므로 접근성 측면에서 주의해서 사용하세요.
+ */
+export function Link({
+  children,
+  tone,
+  size,
+  weight,
+  muted,
+  underline,
+  target,
+  rel,
+  ...props
+}: PropsWithChildren<LinkProps>) {
+  return (
+    <a
+      className={css(
+        styles.raw({ tone, muted, underline }),
+        css.raw({
+          fontSize: size,
+          fontWeight: weight,
+        })
+      )}
+      target={target}
+      rel={target === "_blank" ? "noopener noreferrer" : rel}
+      {...props}
+    >
+      {children}
+    </a>
+  );
 }
+
+const styles = cva({
+  base: {
+    transition: "colors 0.2s",
+  },
+  variants: {
+    tone: {
+      neutral: {},
+      accent: {},
+      danger: {},
+      warning: {},
+    },
+    muted: {
+      true: {},
+      false: {},
+    },
+    underline: {
+      true: {},
+      false: {},
+    },
+  },
+  compoundVariants: [
+    {
+      tone: "neutral",
+      muted: false,
+      css: {
+        color: "text",
+        "&:active, &:hover": {
+          color: "text.muted",
+        },
+      },
+    },
+    {
+      tone: "accent",
+      muted: false,
+      css: {
+        color: "text.accent",
path: src/components/Link/Link.tsx
user: login: hyoseong1994
body: <img width="213" alt="image" src="https://github.com/user-attachments/assets/c3aa7614-c660-4bef-ba1d-0b6793894a36" />


이런 색상이 나오네요  `Mismatch` 스토리 추가했습니다.
created_at: 2025-04-21T06:22:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -15,6 +16,159 @@ interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
   underline?: boolean;
 }
 
-export function Link({ children }: PropsWithChildren<LinkProps>) {
-  return <>{children}</>;
+/**
+ * - `underline` 속성으로 밑줄 표시 여부를 설정할 수 있습니다.
+ * - `target="_blank"`를 사용할 때는 자동으로 보안 속성이 추가됩니다.
+ * - `tone` 속성을 통해서 링크의 색상을 변경할 수 있습니다.
+ * - `size` 속성과 `weight` 속성을 통해서 텍스트 스타일을 변경할 수 있습니다.
+ * - `muted` 속성을 주시면 글자색이 옅어집니다. 명암비가 낮아지므로 접근성 측면에서 주의해서 사용하세요.
+ */
+export function Link({
+  children,
+  tone,
+  size,
+  weight,
+  muted,
+  underline,
+  target,
+  rel,
+  ...props
+}: PropsWithChildren<LinkProps>) {
+  return (
+    <a
+      className={css(
+        styles.raw({ tone, muted, underline }),
+        css.raw({
+          fontSize: size,
path: src/components/Link/Link.tsx
user: login: hyoseong1994
body: <img width="186" alt="image" src="https://github.com/user-attachments/assets/6726865d-8f74-412b-9dd6-776e7a2b9db4" />


아이콘때문에 링크의 크기가 커지네요 구분의 편의를 위해 색상도 다르게 설정하여 `Mismatch` 스토리에 추가했습니다.
created_at: 2025-04-21T06:23:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -15,6 +16,159 @@ interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
   underline?: boolean;
 }
 
-export function Link({ children }: PropsWithChildren<LinkProps>) {
-  return <>{children}</>;
+/**
+ * - `underline` 속성으로 밑줄 표시 여부를 설정할 수 있습니다.
+ * - `target="_blank"`를 사용할 때는 자동으로 보안 속성이 추가됩니다.
+ * - `tone` 속성을 통해서 링크의 색상을 변경할 수 있습니다.
+ * - `size` 속성과 `weight` 속성을 통해서 텍스트 스타일을 변경할 수 있습니다.
+ * - `muted` 속성을 주시면 글자색이 옅어집니다. 명암비가 낮아지므로 접근성 측면에서 주의해서 사용하세요.
+ */
+export function Link({
+  children,
+  tone,
+  size,
+  weight,
+  muted,
+  underline,
+  target,
+  rel,
+  ...props
+}: PropsWithChildren<LinkProps>) {
+  return (
+    <a
+      className={css(
+        styles.raw({ tone, muted, underline }),
+        css.raw({
+          fontSize: size,
+          fontWeight: weight,
+        })
+      )}
+      target={target}
+      rel={target === "_blank" ? "noopener noreferrer" : rel}
+      {...props}
+    >
+      {children}
+    </a>
+  );
 }
+
+const styles = cva({
+  base: {
+    transition: "colors 0.2s",
+  },
+  variants: {
+    tone: {
+      neutral: {},
+      accent: {},
+      danger: {},
+      warning: {},
+    },
+    muted: {
+      true: {},
+      false: {},
+    },
+    underline: {
+      true: {},
+      false: {},
+    },
+  },
+  compoundVariants: [
+    {
+      tone: "neutral",
+      muted: false,
+      css: {
+        color: "text",
+        "&:active, &:hover": {
+          color: "text.muted",
+        },
+      },
+    },
+    {
+      tone: "accent",
+      muted: false,
+      css: {
+        color: "text.accent",
path: src/components/Link/Link.tsx
user: login: DaleSeo
body: @hyoseong1994 이러한 Mismatch는 분명 의도되지 않은 것으로 생각되는데, 스토리로 추가할 것이 아니라 좀 더 고민해보셔야 하지 않을까요? 🤔 사용자가 UI를 구현할 때 언제 이러한 Mismatch를 원할까요? 일관적인 UI를 구현하는데 이러한 기능이 과연 도움이 될까요?
created_at: 2025-04-22T00:49:24Z
author_association: MEMBER
diff_hunk: @@ -15,6 +16,159 @@ interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
   underline?: boolean;
 }
 
-export function Link({ children }: PropsWithChildren<LinkProps>) {
-  return <>{children}</>;
+/**
+ * - `underline` 속성으로 밑줄 표시 여부를 설정할 수 있습니다.
+ * - `target="_blank"`를 사용할 때는 자동으로 보안 속성이 추가됩니다.
+ * - `tone` 속성을 통해서 링크의 색상을 변경할 수 있습니다.
+ * - `size` 속성과 `weight` 속성을 통해서 텍스트 스타일을 변경할 수 있습니다.
+ * - `muted` 속성을 주시면 글자색이 옅어집니다. 명암비가 낮아지므로 접근성 측면에서 주의해서 사용하세요.
+ */
+export function Link({
+  children,
+  tone,
+  size,
+  weight,
+  muted,
+  underline,
+  target,
+  rel,
+  ...props
+}: PropsWithChildren<LinkProps>) {
+  return (
+    <a
+      className={css(
+        styles.raw({ tone, muted, underline }),
+        css.raw({
+          fontSize: size,
path: src/components/Link/Link.tsx
user: login: DaleSeo
body: 위와 동일한 피드백을 드리고 싶습니다. 고민을 좀 해보셨으면 해서 질문을 드렸는데 의도가 잘 전달되지 않은 것 같네요. 😅
created_at: 2025-04-22T00:50:48Z
author_association: MEMBER
diff_hunk: @@ -15,6 +16,159 @@ interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
   underline?: boolean;
 }
 
-export function Link({ children }: PropsWithChildren<LinkProps>) {
-  return <>{children}</>;
+/**
+ * - `underline` 속성으로 밑줄 표시 여부를 설정할 수 있습니다.
+ * - `target="_blank"`를 사용할 때는 자동으로 보안 속성이 추가됩니다.
+ * - `tone` 속성을 통해서 링크의 색상을 변경할 수 있습니다.
+ * - `size` 속성과 `weight` 속성을 통해서 텍스트 스타일을 변경할 수 있습니다.
+ * - `muted` 속성을 주시면 글자색이 옅어집니다. 명암비가 낮아지므로 접근성 측면에서 주의해서 사용하세요.
+ */
+export function Link({
+  children,
+  tone,
+  size,
+  weight,
+  muted,
+  underline,
+  target,
+  rel,
+  ...props
+}: PropsWithChildren<LinkProps>) {
+  return (
+    <a
+      className={css(
+        styles.raw({ tone, muted, underline }),
+        css.raw({
+          fontSize: size,
+          fontWeight: weight,
+        })
+      )}
+      target={target}
+      rel={target === "_blank" ? "noopener noreferrer" : rel}
+      {...props}
+    >
+      {children}
+    </a>
+  );
 }
+
+const styles = cva({
+  base: {
+    transition: "colors 0.2s",
+  },
+  variants: {
+    tone: {
+      neutral: {},
+      accent: {},
+      danger: {},
+      warning: {},
+    },
+    muted: {
+      true: {},
+      false: {},
+    },
+    underline: {
+      true: {},
+      false: {},
+    },
+  },
+  compoundVariants: [
+    {
+      tone: "neutral",
+      muted: false,
+      css: {
+        color: "text",
+        "&:active, &:hover": {
+          color: "text.muted",
+        },
+      },
+    },
+    {
+      tone: "accent",
+      muted: false,
+      css: {
+        color: "text.accent",
path: src/components/Link/Link.tsx
user: login: hyoseong1994
body: 제가 리뷰의 의도를 잘못 파악했네요 다른방안을 강구해보도록하겠습니다.
created_at: 2025-04-22T01:24:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -15,6 +16,176 @@ interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
   underline?: boolean;
 }
 
-export function Link({ children }: PropsWithChildren<LinkProps>) {
-  return <>{children}</>;
+/**
+ * - `underline` 속성으로 밑줄 표시 여부를 설정할 수 있습니다. 기본값은 `true`입니다.
+ * - `target="_blank"`를 사용할 때는 자동으로 보안 속성이 추가됩니다.
+ * - `tone` 속성을 통해서 링크의 색상을 변경할 수 있습니다. 기본값은 `neutral`입니다.
+ * - `size` 속성과 `weight` 속성을 통해서 텍스트 스타일을 변경할 수 있습니다.
+ * - `muted` 속성을 주시면 글자색이 옅어집니다. 명암비가 낮아지므로 접근성 측면에서 주의해서 사용하세요. 기본값은 `false`입니다.
+ */
+export function Link({
+  children,
+  tone = "neutral",
+  size,
+  weight,
+  muted = false,
+  underline = true,
+  target,
+  rel,
+  onClick,
+  ...props
+}: PropsWithChildren<LinkProps>) {
+  const handleKeyDown = (e: React.KeyboardEvent<HTMLAnchorElement>) => {
+    if (e.key === "Enter" && props.href) {
+      if (onClick) {
+        onClick({} as React.MouseEvent<HTMLAnchorElement>);
+      } else {
+        window.location.href = props.href;
+      }
+    }
+  };
+
+  const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
+    if (onClick) onClick(e);
path: src/components/Link/Link.tsx
user: login: sounmind
body: nitpick: 이렇게 작성할 수도 있습니다!
```suggestion
    onClick?.(e);
```
created_at: 2025-04-22T04:00:52Z
author_association: MEMBER
diff_hunk: @@ -15,6 +16,176 @@ interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
   underline?: boolean;
 }
 
-export function Link({ children }: PropsWithChildren<LinkProps>) {
-  return <>{children}</>;
+/**
+ * - `underline` 속성으로 밑줄 표시 여부를 설정할 수 있습니다. 기본값은 `true`입니다.
+ * - `target="_blank"`를 사용할 때는 자동으로 보안 속성이 추가됩니다.
+ * - `tone` 속성을 통해서 링크의 색상을 변경할 수 있습니다. 기본값은 `neutral`입니다.
+ * - `size` 속성과 `weight` 속성을 통해서 텍스트 스타일을 변경할 수 있습니다.
+ * - `muted` 속성을 주시면 글자색이 옅어집니다. 명암비가 낮아지므로 접근성 측면에서 주의해서 사용하세요. 기본값은 `false`입니다.
+ */
+export function Link({
+  children,
+  tone = "neutral",
+  size,
+  weight,
+  muted = false,
+  underline = true,
+  target,
+  rel,
+  onClick,
+  ...props
+}: PropsWithChildren<LinkProps>) {
+  const handleKeyDown = (e: React.KeyboardEvent<HTMLAnchorElement>) => {
+    if (e.key === "Enter" && props.href) {
+      if (onClick) {
+        onClick({} as React.MouseEvent<HTMLAnchorElement>);
path: src/components/Link/Link.tsx
user: login: sounmind
body: 빈 객체를 전달하는 이유가 따로 있을까요?
created_at: 2025-04-22T04:01:22Z
author_association: MEMBER
diff_hunk: @@ -15,6 +16,176 @@ interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
   underline?: boolean;
 }
 
-export function Link({ children }: PropsWithChildren<LinkProps>) {
-  return <>{children}</>;
+/**
+ * - `underline` 속성으로 밑줄 표시 여부를 설정할 수 있습니다. 기본값은 `true`입니다.
+ * - `target="_blank"`를 사용할 때는 자동으로 보안 속성이 추가됩니다.
+ * - `tone` 속성을 통해서 링크의 색상을 변경할 수 있습니다. 기본값은 `neutral`입니다.
+ * - `size` 속성과 `weight` 속성을 통해서 텍스트 스타일을 변경할 수 있습니다.
+ * - `muted` 속성을 주시면 글자색이 옅어집니다. 명암비가 낮아지므로 접근성 측면에서 주의해서 사용하세요. 기본값은 `false`입니다.
+ */
+export function Link({
+  children,
+  tone = "neutral",
+  size,
+  weight,
+  muted = false,
+  underline = true,
+  target,
+  rel,
+  onClick,
+  ...props
+}: PropsWithChildren<LinkProps>) {
+  const handleKeyDown = (e: React.KeyboardEvent<HTMLAnchorElement>) => {
+    if (e.key === "Enter" && props.href) {
+      if (onClick) {
+        onClick({} as React.MouseEvent<HTMLAnchorElement>);
+      } else {
+        window.location.href = props.href;
+      }
+    }
+  };
+
+  const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
+    if (onClick) onClick(e);
path: src/components/Link/Link.tsx
user: login: hyoseong1994
body: 옵셔널체이닝 방식이 더 간결하네요 감사합니다.
created_at: 2025-04-22T05:48:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -15,6 +16,176 @@ interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
   underline?: boolean;
 }
 
-export function Link({ children }: PropsWithChildren<LinkProps>) {
-  return <>{children}</>;
+/**
+ * - `underline` 속성으로 밑줄 표시 여부를 설정할 수 있습니다. 기본값은 `true`입니다.
+ * - `target="_blank"`를 사용할 때는 자동으로 보안 속성이 추가됩니다.
+ * - `tone` 속성을 통해서 링크의 색상을 변경할 수 있습니다. 기본값은 `neutral`입니다.
+ * - `size` 속성과 `weight` 속성을 통해서 텍스트 스타일을 변경할 수 있습니다.
+ * - `muted` 속성을 주시면 글자색이 옅어집니다. 명암비가 낮아지므로 접근성 측면에서 주의해서 사용하세요. 기본값은 `false`입니다.
+ */
+export function Link({
+  children,
+  tone = "neutral",
+  size,
+  weight,
+  muted = false,
+  underline = true,
+  target,
+  rel,
+  onClick,
+  ...props
+}: PropsWithChildren<LinkProps>) {
+  const handleKeyDown = (e: React.KeyboardEvent<HTMLAnchorElement>) => {
+    if (e.key === "Enter" && props.href) {
+      if (onClick) {
+        onClick({} as React.MouseEvent<HTMLAnchorElement>);
path: src/components/Link/Link.tsx
user: login: hyoseong1994
body: 처음에는 `AnchorHTMLAttributes<HTMLAnchorElement> ` 의  `onClick`을 변경해서 사용할까 고민했지만 API surface를 줄여야한다는 얘기를 듣고 `AnchorHTMLAttributes<HTMLAnchorElement> ` 의  `onClick`를 그대로 사용하기로 하였습니다.

따라서 `React.KeyboardEvent<HTMLAnchorElement>`는 `onClick`에 인수로 사용할수없고 
`event: React.MouseEvent<HTMLAnchorElement, MouseEvent>`는 필수값이어서 빈객체를 넣고 타입캐스팅으로 처리하였습니다.
created_at: 2025-04-22T05:54:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -15,6 +16,159 @@ interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
   underline?: boolean;
 }
 
-export function Link({ children }: PropsWithChildren<LinkProps>) {
-  return <>{children}</>;
+/**
+ * - `underline` 속성으로 밑줄 표시 여부를 설정할 수 있습니다.
+ * - `target="_blank"`를 사용할 때는 자동으로 보안 속성이 추가됩니다.
+ * - `tone` 속성을 통해서 링크의 색상을 변경할 수 있습니다.
+ * - `size` 속성과 `weight` 속성을 통해서 텍스트 스타일을 변경할 수 있습니다.
+ * - `muted` 속성을 주시면 글자색이 옅어집니다. 명암비가 낮아지므로 접근성 측면에서 주의해서 사용하세요.
+ */
+export function Link({
+  children,
+  tone,
+  size,
+  weight,
+  muted,
+  underline,
+  target,
+  rel,
+  ...props
+}: PropsWithChildren<LinkProps>) {
+  return (
+    <a
+      className={css(
+        styles.raw({ tone, muted, underline }),
+        css.raw({
+          fontSize: size,
+          fontWeight: weight,
+        })
+      )}
+      target={target}
+      rel={target === "_blank" ? "noopener noreferrer" : rel}
+      {...props}
+    >
+      {children}
+    </a>
+  );
 }
+
+const styles = cva({
+  base: {
+    transition: "colors 0.2s",
+  },
+  variants: {
+    tone: {
+      neutral: {},
+      accent: {},
+      danger: {},
+      warning: {},
+    },
+    muted: {
+      true: {},
+      false: {},
+    },
+    underline: {
+      true: {},
+      false: {},
+    },
+  },
+  compoundVariants: [
+    {
+      tone: "neutral",
+      muted: false,
+      css: {
+        color: "text",
+        "&:active, &:hover": {
+          color: "text.muted",
+        },
+      },
+    },
+    {
+      tone: "accent",
+      muted: false,
+      css: {
+        color: "text.accent",
path: src/components/Link/Link.tsx
user: login: hyoseong1994
body: 초기에 children을 사용해 Link 컴포넌트의 유연성을 높이려 했지만,
달레님이 제안해주신 
- 아이콘과 링크의 크기가 다르거나,
- 텍스트와 링크의 색상이 불일치하는 등

UI 이슈들이 발생해 오히려 일관된 스타일을 유지하기 어려울것으로 판단되어 유연성을 다소 제한하더라도 명확한 구조를 갖추기 위해 children을 제거하고 label과 iconName을 prop으로 분리하여 전달하는 방식으로 수정했습니다.
created_at: 2025-04-22T15:21:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -15,6 +16,159 @@ interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
   underline?: boolean;
 }
 
-export function Link({ children }: PropsWithChildren<LinkProps>) {
-  return <>{children}</>;
+/**
+ * - `underline` 속성으로 밑줄 표시 여부를 설정할 수 있습니다.
+ * - `target="_blank"`를 사용할 때는 자동으로 보안 속성이 추가됩니다.
+ * - `tone` 속성을 통해서 링크의 색상을 변경할 수 있습니다.
+ * - `size` 속성과 `weight` 속성을 통해서 텍스트 스타일을 변경할 수 있습니다.
+ * - `muted` 속성을 주시면 글자색이 옅어집니다. 명암비가 낮아지므로 접근성 측면에서 주의해서 사용하세요.
+ */
+export function Link({
+  children,
+  tone,
+  size,
+  weight,
+  muted,
+  underline,
+  target,
+  rel,
+  ...props
+}: PropsWithChildren<LinkProps>) {
+  return (
+    <a
+      className={css(
+        styles.raw({ tone, muted, underline }),
+        css.raw({
+          fontSize: size,
+          fontWeight: weight,
+        })
+      )}
+      target={target}
+      rel={target === "_blank" ? "noopener noreferrer" : rel}
+      {...props}
+    >
+      {children}
+    </a>
+  );
 }
+
+const styles = cva({
+  base: {
+    transition: "colors 0.2s",
+  },
+  variants: {
+    tone: {
+      neutral: {},
+      accent: {},
+      danger: {},
+      warning: {},
+    },
+    muted: {
+      true: {},
+      false: {},
+    },
+    underline: {
+      true: {},
+      false: {},
+    },
+  },
+  compoundVariants: [
+    {
+      tone: "neutral",
+      muted: false,
+      css: {
+        color: "text",
+        "&:active, &:hover": {
+          color: "text.muted",
+        },
+      },
+    },
+    {
+      tone: "accent",
+      muted: false,
+      css: {
+        color: "text.accent",
path: src/components/Link/Link.tsx
user: login: DaleSeo
body: 수정해주신 API는 디자인 시스템의 링크 컴포넌트라고 보기 어려울 정도로 재사용성이 낮고 확장에 닫혀있는 설계인 것 같습니다... 😓 예를 들어, 이미지에 링크를 걸어야 하면 어떻게 해야 할까요? `imgSrc`와 같은  prop을 추가하실 건가요? 카드와 같이 좀 더 넓은 요소에 링크를 달고 싶으면 어떻게 해야 하나요? 카드와 관련된 prop을 또 추가하실 건가요? 수정된 API는 `<Link/>` 컴포넌트 용으로는 부적할 것 같고,  `<TextLink/>`나 `<IconLink/>`와 같이 좀 더 특화된 컴포넌트에 적합한 API라고 생각합니다.

제가 드린 피드백에 대해서 별다른 추가 논의도 없이 이렇게 임의로 설계를 크게 바꾸면 안 될 것 같습니다. 구현 단계에서 이렇게 쉽게 API를 바꿀 거라면 설계 티켓을 따로 만들고 바람직한 API에 대해 함께 고민하고 합의한 과정의 의미가 퇴색될 거라 생각합니다. 그리고 코드를 수정하시기 전에 @hyoseong1994 님의 생각에 대해서 미리 소통을 했더라면 불필요한 구현 비용도 아낄 수 있었을텐데 많이 안타깝습니다 😭

일단 기존 API로 복구하고 Mismatch 문제는 본 PR에서 해결하기에는 너무 지연이 되고 있는 것 같으니, 따로 티켓을 만들어주실 수 있으실까요? 티켓에 문제를 잘 정의해주시고 다음 회의 때 다른 팀원들에게 공유해주시면 좋을 것 같습니다.
created_at: 2025-04-23T23:49:00Z
author_association: MEMBER
diff_hunk: @@ -15,6 +16,159 @@ interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
   underline?: boolean;
 }
 
-export function Link({ children }: PropsWithChildren<LinkProps>) {
-  return <>{children}</>;
+/**
+ * - `underline` 속성으로 밑줄 표시 여부를 설정할 수 있습니다.
+ * - `target="_blank"`를 사용할 때는 자동으로 보안 속성이 추가됩니다.
+ * - `tone` 속성을 통해서 링크의 색상을 변경할 수 있습니다.
+ * - `size` 속성과 `weight` 속성을 통해서 텍스트 스타일을 변경할 수 있습니다.
+ * - `muted` 속성을 주시면 글자색이 옅어집니다. 명암비가 낮아지므로 접근성 측면에서 주의해서 사용하세요.
+ */
+export function Link({
+  children,
+  tone,
+  size,
+  weight,
+  muted,
+  underline,
+  target,
+  rel,
+  ...props
+}: PropsWithChildren<LinkProps>) {
+  return (
+    <a
+      className={css(
+        styles.raw({ tone, muted, underline }),
+        css.raw({
+          fontSize: size,
path: src/components/Link/Link.tsx
user: login: Sunjae95
body: @DaleSeo @hyoseong1994 님의 대화를 보면서 Link에 Icon이 존재할 때 크기는 어떻게 되어야하는가를 고민한걸 공유하고자 코멘트 남깁니다.

가장 먼저 떠오르는건 Link 컴포넌트 자식으로 Icon 컴포넌트가 존재할 때, Icon 컴포넌트의 사이즈가 Link보다 크게되면 console.warn이나 throw error로 개발환경에서 에러를 내주는 방법이 떠올랐어요.
그러면서 `좋은설계 API` 에서 말씀하신 Link 컴포넌트를  `컴파운드 패턴을 적용하여 Icon의 사이즈를 조절하는건 어떨까?`도 생각드네요.

이렇게 되면 모든 자식 컴포넌트에 사이즈를 확인하는 코드베이스가 추가되어 커지고, 디자이너와 사이즈관련 소통(현재는 예시케이스이므로 어려움) 필요하여 작업의 사이즈가 방대해져 이 작업에서 쉽게 결정하지 못한다고 예상하여 쉽게 정하지 못하겠네요.
created_at: 2025-04-23T23:49:49Z
author_association: MEMBER
diff_hunk: @@ -1,19 +1,153 @@
-import { test } from "vitest";
+import { faker } from "@faker-js/faker";
+import { composeStories } from "@storybook/react";
+import { render, screen, fireEvent } from "@testing-library/react";
+import { expect, test, vi } from "vitest";
+import { fontSizes, fontWeights } from "../../tokens/typography";
+import * as stories from "./Link.stories";
 
-test.todo("renders children correctly");
+const { Basic, Tones, Contrasts, Underlines, Security, WithIcon } =
+  composeStories(stories);
 
-test.todo("applies the correct tone based on the 'tone' prop");
+test("renders the link with the correct text content", () => {
+  render(<Basic />);
+  expect(screen.getByRole("link")).toHaveTextContent("링크");
+});
 
-test.todo("applies the correct font size based on the 'size' prop");
+test("applies the correct tone based on the 'tone' prop", () => {
+  render(<Tones />);
 
-test.todo("applies the correct font weight based on the 'weight' prop");
+  expect(screen.getByRole("link", { name: "중립 링크" })).toHaveClass("c_text");
+  expect(screen.getByRole("link", { name: "강조 링크" })).toHaveClass(
+    "c_text.accent"
+  );
+  expect(screen.getByRole("link", { name: "위험 링크" })).toHaveClass(
+    "c_text.danger"
+  );
+  expect(screen.getByRole("link", { name: "경고 링크" })).toHaveClass(
+    "c_text.warning"
+  );
+});
 
-test.todo("renders with reduced contrast when 'muted' is true");
+test("applies the correct font size based on the 'size' prop", () => {
+  const size = faker.helpers.arrayElement(
+    Object.keys(fontSizes)
+  ) as keyof typeof fontSizes;
 
-test.todo("renders with underline when 'underline' is true");
+  render(<Basic size={size} />);
+  expect(screen.getByRole("link")).toHaveStyle({ fontSize: size });
+});
 
-test.todo("forwards additional anchor props like 'href' and 'target'");
+test("applies the correct font weight based on the 'weight' prop", () => {
+  const weight = faker.helpers.arrayElement(
+    Object.keys(fontWeights)
+  ) as keyof typeof fontWeights;
 
-test.todo("adds rel='noopener noreferrer' when target is '_blank'");
+  render(<Basic weight={weight} />);
+  expect(screen.getByRole("link")).toHaveClass(`fw_${weight}`);
+});
 
-test.todo("can be used with Icon component");
+test("applies the correct styles for low and high contrast", () => {
+  render(<Contrasts />);
+
+  expect(screen.getByRole("link", { name: "낮은 명암비" })).toHaveClass(
+    "c_text.muted"
+  );
+
+  expect(screen.getByRole("link", { name: "높은 명암비" })).toHaveClass(
+    "c_text"
+  );
+});
+
+test("applies the correct underline styles", () => {
+  render(<Underlines />);
+
+  expect(screen.getByRole("link", { name: "밑줄 있음" })).toHaveClass(
+    "td_underline"
+  );
+  expect(screen.getByRole("link", { name: "밑줄 없음" })).toHaveClass(
+    "td_none"
+  );
+});
+
+test("forwards additional anchor props like 'href' and 'target'", () => {
+  const href = faker.internet.url({ appendSlash: true });
+  const target = "_self";
+
+  render(<Basic href={href} target={target} />);
+
+  const link = screen.getByRole("link");
+  expect(link).toHaveAttribute("href", href);
+  expect(link).toHaveAttribute("target", target);
+});
+
+test("adds rel='noopener noreferrer' when target is '_blank'", () => {
+  render(<Security />);
+
+  expect(
+    screen.getByRole("link", { name: "새 탭에서 열기 (보안 속성 자동 추가)" })
+  ).toHaveAttribute("rel", "noopener noreferrer");
+
+  expect(
+    screen.getByRole("link", { name: "같은 탭에서 열기" })
+  ).not.toHaveAttribute("rel", "noopener noreferrer");
+});
+
+test("can be used with Icon component", () => {
+  render(<WithIcon />);
+
+  const link = screen.getByRole("link");
+  expect(link).toHaveTextContent("링크");
+  expect(link.querySelector("svg")).toBeInTheDocument();
+});
+
+test("navigates to the correct URL when clicked", () => {
+  const href = faker.internet.url({ appendSlash: true });
+  render(<Basic href={href} onClick={undefined} />);
+
+  const link = screen.getByRole("link");
+  expect(link).toHaveAttribute("href", href);
+  fireEvent.click(link);
path: src/components/Link/Link.test.tsx
user: login: DaleSeo
body: Testing Library에서는 `fireEvent` 대신에 `@testing-library/user-event` 패키지를 사용하라고 권고하고 있습니다. 관련해서는 제가 블로그에서 자세히 다루고 있으니 참고바랍니다: https://www.daleseo.com/testing-library-user-agent/
created_at: 2025-04-23T23:54:03Z
author_association: MEMBER
diff_hunk: @@ -13,8 +18,180 @@ interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
   muted?: boolean;
   /** 링크에 밑줄 표시 여부 */
   underline?: boolean;
+  /** 링크 아이콘 */
+  iconName?: IconName;
 }
 
-export function Link({ children }: PropsWithChildren<LinkProps>) {
-  return <>{children}</>;
+/**
+ * - `label` 속성으로 링크의 텍스트를 설정할 수 있습니다.
+ * - `underline` 속성으로 밑줄 표시 여부를 설정할 수 있습니다. 기본값은 `true`입니다.
+ * - `target="_blank"`를 사용할 때는 자동으로 보안 속성이 추가됩니다.
+ * - `tone` 속성을 통해서 링크의 색상을 변경할 수 있습니다. 기본값은 `neutral`입니다.
+ * - `size` 속성과 `weight` 속성을 통해서 텍스트 스타일을 변경할 수 있습니다.
+ * - `muted` 속성을 주시면 글자색이 옅어집니다. 명암비가 낮아지므로 접근성 측면에서 주의해서 사용하세요. 기본값은 `false`입니다.
+ * - `iconName` 속성을 통해서 링크에 아이콘을 추가할 수 있습니다.
+ */
+export function Link({
+  label,
+  tone = "neutral",
+  size,
+  weight,
+  muted = false,
+  underline = true,
+  target,
+  rel,
+  onClick,
+  iconName,
+  ...props
+}: LinkProps) {
+  const handleKeyDown = (e: React.KeyboardEvent<HTMLAnchorElement>) => {
+    if (e.key === "Enter" && props.href) {
+      if (onClick) {
+        onClick({} as React.MouseEvent<HTMLAnchorElement>);
+      } else {
+        window.location.href = props.href;
+      }
+    }
+  };
+
+  return (
+    <a
+      className={css(
+        styles.raw({ tone, muted, underline }),
+        css.raw({
+          fontSize: size,
+          fontWeight: weight,
+        })
+      )}
+      target={target}
+      rel={target === "_blank" ? "noopener noreferrer" : rel}
+      onClick={onClick}
+      onKeyDown={handleKeyDown}
path: src/components/Link/Link.tsx
user: login: DaleSeo
body: 이것은 불필요한 구현입니다. 테스트에서 `fireEvent`를 이용하여 `keyDown` 이벤트만 발생시켰는데요. 사용자가 엔터키를 누를 때는 `keyDown` 이벤트뿐만 아니라 다른 이벤트도 발생합니다. 브라우저의 입장이 아닌 키보드 사용자 입장에서 테스트를 작성해주세요. 탭키를 눌러서 링크로 우선 이동하고 그다음에 엔터키를 누르도록 테스트를 작성하면 이러한 이벤트 핸들러 바인딩이 필요 없다는 것을 알게 되실 겁니다.
created_at: 2025-04-24T00:03:02Z
author_association: MEMBER
diff_hunk: @@ -15,6 +16,159 @@ interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
   underline?: boolean;
 }
 
-export function Link({ children }: PropsWithChildren<LinkProps>) {
-  return <>{children}</>;
+/**
+ * - `underline` 속성으로 밑줄 표시 여부를 설정할 수 있습니다.
+ * - `target="_blank"`를 사용할 때는 자동으로 보안 속성이 추가됩니다.
+ * - `tone` 속성을 통해서 링크의 색상을 변경할 수 있습니다.
+ * - `size` 속성과 `weight` 속성을 통해서 텍스트 스타일을 변경할 수 있습니다.
+ * - `muted` 속성을 주시면 글자색이 옅어집니다. 명암비가 낮아지므로 접근성 측면에서 주의해서 사용하세요.
+ */
+export function Link({
+  children,
+  tone,
+  size,
+  weight,
+  muted,
+  underline,
+  target,
+  rel,
+  ...props
+}: PropsWithChildren<LinkProps>) {
+  return (
+    <a
+      className={css(
+        styles.raw({ tone, muted, underline }),
+        css.raw({
+          fontSize: size,
+          fontWeight: weight,
+        })
+      )}
+      target={target}
+      rel={target === "_blank" ? "noopener noreferrer" : rel}
+      {...props}
+    >
+      {children}
+    </a>
+  );
 }
+
+const styles = cva({
+  base: {
+    transition: "colors 0.2s",
+  },
+  variants: {
+    tone: {
+      neutral: {},
+      accent: {},
+      danger: {},
+      warning: {},
+    },
+    muted: {
+      true: {},
+      false: {},
+    },
+    underline: {
+      true: {},
+      false: {},
+    },
+  },
+  compoundVariants: [
+    {
+      tone: "neutral",
+      muted: false,
+      css: {
+        color: "text",
+        "&:active, &:hover": {
+          color: "text.muted",
+        },
+      },
+    },
+    {
+      tone: "accent",
+      muted: false,
+      css: {
+        color: "text.accent",
path: src/components/Link/Link.tsx
user: login: hyoseong1994
body: 이번 스프린트 내로 끝내려는 마음에 급급하여 가장 기본적인 부분을 놓쳤네요 죄송합니다. :pensive:

피드백 주신 부분을 해결하여 고민하여 나온 결론이 2개가 있었습니다.

1. children을 순회하면서 크기, 색상 등 강제 변환
2. props 변경으로  유연성을 제한

이 두 가지였는데 로우레벨 컴포넌트에서 순회한다는건 낭비가 심할것이라고 판단되어 2번을 선택하여 작업하게되었습니다.

제 욕심때문에 소통에 부재가 있던점 죄송합니다. 앞으로는 소통을 먼저하는 방향으로 작업하도록 하겠습니다. 
API 변경건은 되돌리도록 하겠습니다.

Mismatch 티켓 또한 문제 정리하여 만들도록하겠습니다.

created_at: 2025-04-24T00:31:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -15,6 +16,159 @@ interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
   underline?: boolean;
 }
 
-export function Link({ children }: PropsWithChildren<LinkProps>) {
-  return <>{children}</>;
+/**
+ * - `underline` 속성으로 밑줄 표시 여부를 설정할 수 있습니다.
+ * - `target="_blank"`를 사용할 때는 자동으로 보안 속성이 추가됩니다.
+ * - `tone` 속성을 통해서 링크의 색상을 변경할 수 있습니다.
+ * - `size` 속성과 `weight` 속성을 통해서 텍스트 스타일을 변경할 수 있습니다.
+ * - `muted` 속성을 주시면 글자색이 옅어집니다. 명암비가 낮아지므로 접근성 측면에서 주의해서 사용하세요.
+ */
+export function Link({
+  children,
+  tone,
+  size,
+  weight,
+  muted,
+  underline,
+  target,
+  rel,
+  ...props
+}: PropsWithChildren<LinkProps>) {
+  return (
+    <a
+      className={css(
+        styles.raw({ tone, muted, underline }),
+        css.raw({
+          fontSize: size,
path: src/components/Link/Link.tsx
user: login: hyoseong1994
body: 좋은 의견감사합니다. 

`Link 컴포넌트 자식으로 Icon 컴포넌트가 존재할 때, Icon 컴포넌트의 사이즈가 Link보다 크게되면` 이부분 구현을 해보았는데 로우레벨 컴포넌트인 링크에서 children순회를 돈다는것이 좋지 않은 방법이라고 판단되어 진행하지 않았습니다.

컴파운드 패턴에 대해서는 세모의 의견입니다. 사이즈 조절관점에서는 고정된 값 `md`(link 컴포넌트와 크기가 잘맞음) 을 제공하면 좋겠지만 이미 icon 컴포넌트가 있는데 link 용 icon 컴포넌트를 만든다면 사용자입장에서 혼란스럽지 않을까 우려됩니다.

선재님 생각은 어떠신가요?
created_at: 2025-04-24T00:35:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,19 +1,153 @@
-import { test } from "vitest";
+import { faker } from "@faker-js/faker";
+import { composeStories } from "@storybook/react";
+import { render, screen, fireEvent } from "@testing-library/react";
+import { expect, test, vi } from "vitest";
+import { fontSizes, fontWeights } from "../../tokens/typography";
+import * as stories from "./Link.stories";
 
-test.todo("renders children correctly");
+const { Basic, Tones, Contrasts, Underlines, Security, WithIcon } =
+  composeStories(stories);
 
-test.todo("applies the correct tone based on the 'tone' prop");
+test("renders the link with the correct text content", () => {
+  render(<Basic />);
+  expect(screen.getByRole("link")).toHaveTextContent("링크");
+});
 
-test.todo("applies the correct font size based on the 'size' prop");
+test("applies the correct tone based on the 'tone' prop", () => {
+  render(<Tones />);
 
-test.todo("applies the correct font weight based on the 'weight' prop");
+  expect(screen.getByRole("link", { name: "중립 링크" })).toHaveClass("c_text");
+  expect(screen.getByRole("link", { name: "강조 링크" })).toHaveClass(
+    "c_text.accent"
+  );
+  expect(screen.getByRole("link", { name: "위험 링크" })).toHaveClass(
+    "c_text.danger"
+  );
+  expect(screen.getByRole("link", { name: "경고 링크" })).toHaveClass(
+    "c_text.warning"
+  );
+});
 
-test.todo("renders with reduced contrast when 'muted' is true");
+test("applies the correct font size based on the 'size' prop", () => {
+  const size = faker.helpers.arrayElement(
+    Object.keys(fontSizes)
+  ) as keyof typeof fontSizes;
 
-test.todo("renders with underline when 'underline' is true");
+  render(<Basic size={size} />);
+  expect(screen.getByRole("link")).toHaveStyle({ fontSize: size });
+});
 
-test.todo("forwards additional anchor props like 'href' and 'target'");
+test("applies the correct font weight based on the 'weight' prop", () => {
+  const weight = faker.helpers.arrayElement(
+    Object.keys(fontWeights)
+  ) as keyof typeof fontWeights;
 
-test.todo("adds rel='noopener noreferrer' when target is '_blank'");
+  render(<Basic weight={weight} />);
+  expect(screen.getByRole("link")).toHaveClass(`fw_${weight}`);
+});
 
-test.todo("can be used with Icon component");
+test("applies the correct styles for low and high contrast", () => {
+  render(<Contrasts />);
+
+  expect(screen.getByRole("link", { name: "낮은 명암비" })).toHaveClass(
+    "c_text.muted"
+  );
+
+  expect(screen.getByRole("link", { name: "높은 명암비" })).toHaveClass(
+    "c_text"
+  );
+});
+
+test("applies the correct underline styles", () => {
+  render(<Underlines />);
+
+  expect(screen.getByRole("link", { name: "밑줄 있음" })).toHaveClass(
+    "td_underline"
+  );
+  expect(screen.getByRole("link", { name: "밑줄 없음" })).toHaveClass(
+    "td_none"
+  );
+});
+
+test("forwards additional anchor props like 'href' and 'target'", () => {
+  const href = faker.internet.url({ appendSlash: true });
+  const target = "_self";
+
+  render(<Basic href={href} target={target} />);
+
+  const link = screen.getByRole("link");
+  expect(link).toHaveAttribute("href", href);
+  expect(link).toHaveAttribute("target", target);
+});
+
+test("adds rel='noopener noreferrer' when target is '_blank'", () => {
+  render(<Security />);
+
+  expect(
+    screen.getByRole("link", { name: "새 탭에서 열기 (보안 속성 자동 추가)" })
+  ).toHaveAttribute("rel", "noopener noreferrer");
+
+  expect(
+    screen.getByRole("link", { name: "같은 탭에서 열기" })
+  ).not.toHaveAttribute("rel", "noopener noreferrer");
+});
+
+test("can be used with Icon component", () => {
+  render(<WithIcon />);
+
+  const link = screen.getByRole("link");
+  expect(link).toHaveTextContent("링크");
+  expect(link.querySelector("svg")).toBeInTheDocument();
+});
+
+test("navigates to the correct URL when clicked", () => {
+  const href = faker.internet.url({ appendSlash: true });
+  render(<Basic href={href} onClick={undefined} />);
+
+  const link = screen.getByRole("link");
+  expect(link).toHaveAttribute("href", href);
+  fireEvent.click(link);
path: src/components/Link/Link.test.tsx
user: login: hyoseong1994
body: 넵 변경하도록 하겠습니다. 궁금한점이 있는데 현재 프로젝트에서는 fireEvent만 사용중에 있던데 특별한 이유가 있을까요?
created_at: 2025-04-24T00:37:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -13,8 +18,180 @@ interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
   muted?: boolean;
   /** 링크에 밑줄 표시 여부 */
   underline?: boolean;
+  /** 링크 아이콘 */
+  iconName?: IconName;
 }
 
-export function Link({ children }: PropsWithChildren<LinkProps>) {
-  return <>{children}</>;
+/**
+ * - `label` 속성으로 링크의 텍스트를 설정할 수 있습니다.
+ * - `underline` 속성으로 밑줄 표시 여부를 설정할 수 있습니다. 기본값은 `true`입니다.
+ * - `target="_blank"`를 사용할 때는 자동으로 보안 속성이 추가됩니다.
+ * - `tone` 속성을 통해서 링크의 색상을 변경할 수 있습니다. 기본값은 `neutral`입니다.
+ * - `size` 속성과 `weight` 속성을 통해서 텍스트 스타일을 변경할 수 있습니다.
+ * - `muted` 속성을 주시면 글자색이 옅어집니다. 명암비가 낮아지므로 접근성 측면에서 주의해서 사용하세요. 기본값은 `false`입니다.
+ * - `iconName` 속성을 통해서 링크에 아이콘을 추가할 수 있습니다.
+ */
+export function Link({
+  label,
+  tone = "neutral",
+  size,
+  weight,
+  muted = false,
+  underline = true,
+  target,
+  rel,
+  onClick,
+  iconName,
+  ...props
+}: LinkProps) {
+  const handleKeyDown = (e: React.KeyboardEvent<HTMLAnchorElement>) => {
+    if (e.key === "Enter" && props.href) {
+      if (onClick) {
+        onClick({} as React.MouseEvent<HTMLAnchorElement>);
+      } else {
+        window.location.href = props.href;
+      }
+    }
+  };
+
+  return (
+    <a
+      className={css(
+        styles.raw({ tone, muted, underline }),
+        css.raw({
+          fontSize: size,
+          fontWeight: weight,
+        })
+      )}
+      target={target}
+      rel={target === "_blank" ? "noopener noreferrer" : rel}
+      onClick={onClick}
+      onKeyDown={handleKeyDown}
path: src/components/Link/Link.tsx
user: login: hyoseong1994
body: 넵 브라우저 환경에서는 말씀하신대로 tab과 enter 를 사용하는데 onKeyDown을 바인딩 할 필요없이 자동으로 되지만 테스트환경(jsdom)에서는 작동하지 않더군요 그래서 이벤트 바인딩 관련 코드가 추가되었습니다.

userEvent로는 안해봐서 변경해보고 처리가 되는지 확인해보겠습니다.
created_at: 2025-04-24T00:41:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -13,8 +18,180 @@ interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
   muted?: boolean;
   /** 링크에 밑줄 표시 여부 */
   underline?: boolean;
+  /** 링크 아이콘 */
+  iconName?: IconName;
 }
 
-export function Link({ children }: PropsWithChildren<LinkProps>) {
-  return <>{children}</>;
+/**
+ * - `label` 속성으로 링크의 텍스트를 설정할 수 있습니다.
+ * - `underline` 속성으로 밑줄 표시 여부를 설정할 수 있습니다. 기본값은 `true`입니다.
+ * - `target="_blank"`를 사용할 때는 자동으로 보안 속성이 추가됩니다.
+ * - `tone` 속성을 통해서 링크의 색상을 변경할 수 있습니다. 기본값은 `neutral`입니다.
+ * - `size` 속성과 `weight` 속성을 통해서 텍스트 스타일을 변경할 수 있습니다.
+ * - `muted` 속성을 주시면 글자색이 옅어집니다. 명암비가 낮아지므로 접근성 측면에서 주의해서 사용하세요. 기본값은 `false`입니다.
+ * - `iconName` 속성을 통해서 링크에 아이콘을 추가할 수 있습니다.
+ */
+export function Link({
+  label,
+  tone = "neutral",
+  size,
+  weight,
+  muted = false,
+  underline = true,
+  target,
+  rel,
+  onClick,
+  iconName,
+  ...props
+}: LinkProps) {
+  const handleKeyDown = (e: React.KeyboardEvent<HTMLAnchorElement>) => {
+    if (e.key === "Enter" && props.href) {
+      if (onClick) {
+        onClick({} as React.MouseEvent<HTMLAnchorElement>);
+      } else {
+        window.location.href = props.href;
+      }
+    }
+  };
+
+  return (
+    <a
+      className={css(
+        styles.raw({ tone, muted, underline }),
+        css.raw({
+          fontSize: size,
+          fontWeight: weight,
+        })
+      )}
+      target={target}
+      rel={target === "_blank" ? "noopener noreferrer" : rel}
+      onClick={onClick}
+      onKeyDown={handleKeyDown}
path: src/components/Link/Link.tsx
user: login: hyoseong1994
body: userEvent로 변경하니 이벤트바인딩 없이도 잘 동작하네요 :open_mouth:
감사합니다 
created_at: 2025-04-24T00:50:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,19 +1,153 @@
-import { test } from "vitest";
+import { faker } from "@faker-js/faker";
+import { composeStories } from "@storybook/react";
+import { render, screen, fireEvent } from "@testing-library/react";
+import { expect, test, vi } from "vitest";
+import { fontSizes, fontWeights } from "../../tokens/typography";
+import * as stories from "./Link.stories";
 
-test.todo("renders children correctly");
+const { Basic, Tones, Contrasts, Underlines, Security, WithIcon } =
+  composeStories(stories);
 
-test.todo("applies the correct tone based on the 'tone' prop");
+test("renders the link with the correct text content", () => {
+  render(<Basic />);
+  expect(screen.getByRole("link")).toHaveTextContent("링크");
+});
 
-test.todo("applies the correct font size based on the 'size' prop");
+test("applies the correct tone based on the 'tone' prop", () => {
+  render(<Tones />);
 
-test.todo("applies the correct font weight based on the 'weight' prop");
+  expect(screen.getByRole("link", { name: "중립 링크" })).toHaveClass("c_text");
+  expect(screen.getByRole("link", { name: "강조 링크" })).toHaveClass(
+    "c_text.accent"
+  );
+  expect(screen.getByRole("link", { name: "위험 링크" })).toHaveClass(
+    "c_text.danger"
+  );
+  expect(screen.getByRole("link", { name: "경고 링크" })).toHaveClass(
+    "c_text.warning"
+  );
+});
 
-test.todo("renders with reduced contrast when 'muted' is true");
+test("applies the correct font size based on the 'size' prop", () => {
+  const size = faker.helpers.arrayElement(
+    Object.keys(fontSizes)
+  ) as keyof typeof fontSizes;
 
-test.todo("renders with underline when 'underline' is true");
+  render(<Basic size={size} />);
+  expect(screen.getByRole("link")).toHaveStyle({ fontSize: size });
+});
 
-test.todo("forwards additional anchor props like 'href' and 'target'");
+test("applies the correct font weight based on the 'weight' prop", () => {
+  const weight = faker.helpers.arrayElement(
+    Object.keys(fontWeights)
+  ) as keyof typeof fontWeights;
 
-test.todo("adds rel='noopener noreferrer' when target is '_blank'");
+  render(<Basic weight={weight} />);
+  expect(screen.getByRole("link")).toHaveClass(`fw_${weight}`);
+});
 
-test.todo("can be used with Icon component");
+test("applies the correct styles for low and high contrast", () => {
+  render(<Contrasts />);
+
+  expect(screen.getByRole("link", { name: "낮은 명암비" })).toHaveClass(
+    "c_text.muted"
+  );
+
+  expect(screen.getByRole("link", { name: "높은 명암비" })).toHaveClass(
+    "c_text"
+  );
+});
+
+test("applies the correct underline styles", () => {
+  render(<Underlines />);
+
+  expect(screen.getByRole("link", { name: "밑줄 있음" })).toHaveClass(
+    "td_underline"
+  );
+  expect(screen.getByRole("link", { name: "밑줄 없음" })).toHaveClass(
+    "td_none"
+  );
+});
+
+test("forwards additional anchor props like 'href' and 'target'", () => {
+  const href = faker.internet.url({ appendSlash: true });
+  const target = "_self";
+
+  render(<Basic href={href} target={target} />);
+
+  const link = screen.getByRole("link");
+  expect(link).toHaveAttribute("href", href);
+  expect(link).toHaveAttribute("target", target);
+});
+
+test("adds rel='noopener noreferrer' when target is '_blank'", () => {
+  render(<Security />);
+
+  expect(
+    screen.getByRole("link", { name: "새 탭에서 열기 (보안 속성 자동 추가)" })
+  ).toHaveAttribute("rel", "noopener noreferrer");
+
+  expect(
+    screen.getByRole("link", { name: "같은 탭에서 열기" })
+  ).not.toHaveAttribute("rel", "noopener noreferrer");
+});
+
+test("can be used with Icon component", () => {
+  render(<WithIcon />);
+
+  const link = screen.getByRole("link");
+  expect(link).toHaveTextContent("링크");
+  expect(link.querySelector("svg")).toBeInTheDocument();
+});
+
+test("navigates to the correct URL when clicked", () => {
+  const href = faker.internet.url({ appendSlash: true });
+  render(<Basic href={href} onClick={undefined} />);
+
+  const link = screen.getByRole("link");
+  expect(link).toHaveAttribute("href", href);
+  fireEvent.click(link);
path: src/components/Link/Link.test.tsx
user: login: DaleSeo
body: @hyoseong1994 없습니다. 제가 코드 리뷰할 때 미처 못 챙긴 것 같습니다. 테스트 코드 일괄 수정할 수 있도록 티켓 #194 생성하였습니다.
created_at: 2025-04-24T00:58:48Z
author_association: MEMBER
diff_hunk: @@ -1,19 +1,143 @@
-import { test } from "vitest";
+import { faker } from "@faker-js/faker";
+import { composeStories } from "@storybook/react";
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { expect, test, vi } from "vitest";
+import { fontSizes, fontWeights } from "../../tokens/typography";
+import * as stories from "./Link.stories";
 
-test.todo("renders children correctly");
+const { Basic, Tones, Contrasts, Underlines, Security, WithIcon } =
+  composeStories(stories);
 
-test.todo("applies the correct tone based on the 'tone' prop");
+test("renders the link with the correct text content", () => {
+  render(<Basic />);
+  expect(screen.getByRole("link")).toHaveTextContent("링크");
+});
 
-test.todo("applies the correct font size based on the 'size' prop");
+test("applies the correct tone based on the 'tone' prop", () => {
+  render(<Tones />);
 
-test.todo("applies the correct font weight based on the 'weight' prop");
+  expect(screen.getByRole("link", { name: "중립 링크" })).toHaveClass("c_text");
+  expect(screen.getByRole("link", { name: "강조 링크" })).toHaveClass(
+    "c_text.accent"
+  );
+  expect(screen.getByRole("link", { name: "위험 링크" })).toHaveClass(
+    "c_text.danger"
+  );
+  expect(screen.getByRole("link", { name: "경고 링크" })).toHaveClass(
+    "c_text.warning"
+  );
+});
 
-test.todo("renders with reduced contrast when 'muted' is true");
+test("applies the correct font size based on the 'size' prop", () => {
+  const size = faker.helpers.arrayElement(
+    Object.keys(fontSizes)
+  ) as keyof typeof fontSizes;
 
-test.todo("renders with underline when 'underline' is true");
+  render(<Basic size={size} />);
+  expect(screen.getByRole("link")).toHaveStyle({ fontSize: size });
+});
 
-test.todo("forwards additional anchor props like 'href' and 'target'");
+test("applies the correct font weight based on the 'weight' prop", () => {
+  const weight = faker.helpers.arrayElement(
+    Object.keys(fontWeights)
+  ) as keyof typeof fontWeights;
 
-test.todo("adds rel='noopener noreferrer' when target is '_blank'");
+  render(<Basic weight={weight} />);
+  expect(screen.getByRole("link")).toHaveClass(`fw_${weight}`);
+});
 
-test.todo("can be used with Icon component");
+test("applies the correct styles for low and high contrast", () => {
+  render(<Contrasts />);
+
+  expect(screen.getByRole("link", { name: "낮은 명암비" })).toHaveClass(
+    "c_text.muted"
+  );
+
+  expect(screen.getByRole("link", { name: "높은 명암비" })).toHaveClass(
+    "c_text"
+  );
+});
+
+test("applies the correct underline styles", () => {
+  render(<Underlines />);
+
+  expect(screen.getByRole("link", { name: "밑줄 있음" })).toHaveClass(
+    "td_underline"
+  );
+  expect(screen.getByRole("link", { name: "밑줄 없음" })).toHaveClass(
+    "td_none"
+  );
+});
+
+test("forwards additional anchor props like 'href' and 'target'", () => {
+  const href = faker.internet.url({ appendSlash: true });
+  const target = "_self";
+
+  render(<Basic href={href} target={target} />);
+
+  const link = screen.getByRole("link");
+  expect(link).toHaveAttribute("href", href);
+  expect(link).toHaveAttribute("target", target);
+});
+
+test("adds rel='noopener noreferrer' when target is '_blank'", () => {
+  render(<Security />);
+
+  expect(
+    screen.getByRole("link", { name: "새 탭에서 열기 (보안 속성 자동 추가)" })
+  ).toHaveAttribute("rel", "noopener noreferrer");
+
+  expect(
+    screen.getByRole("link", { name: "같은 탭에서 열기" })
+  ).not.toHaveAttribute("rel", "noopener noreferrer");
+});
+
+test("can be used with Icon component", () => {
+  render(<WithIcon />);
+
+  const link = screen.getByRole("link");
+  expect(link).toHaveTextContent("링크");
+  expect(link.querySelector("svg")).toBeInTheDocument();
+});
+
+test("navigates to the correct URL when clicked", async () => {
+  const href = faker.internet.url({ appendSlash: true });
+  render(<Basic href={href} onClick={undefined} />);
+
+  const link = screen.getByRole("link");
+  expect(link).toHaveAttribute("href", href);
+  await userEvent.click(link);
+
+  expect(window.location.href).toBe(href);
+});
+
+test("handles click events correctly", async () => {
+  const handleClick = vi.fn();
+  render(<Basic href="#" onClick={handleClick} />);
+
+  const link = screen.getByRole("link");
+  await userEvent.click(link);
+
+  expect(handleClick).toHaveBeenCalledTimes(1);
+});
+
+test("link is keyboard focusable", () => {
+  const href = faker.internet.url({ appendSlash: true });
+  render(<Basic href={href} />);
+
+  const link = screen.getByRole("link");
+  link.focus();
+  expect(link).toHaveFocus();
+});
+
+test("navigates when link is clicked with keyboard", async () => {
+  const href = faker.internet.url({ appendSlash: true });
+  render(<Basic href={href} onClick={undefined} />);
+
+  const link = screen.getByRole("link");
+  link.focus();
path: src/components/Link/Link.test.tsx
user: login: DaleSeo
body: 키보드 사용자는 이런 식으로 링크로 선택할 수 없습니다.

```suggestion
  await userEvent.tab()
```

@hyoseong1994 님, 아직도 뭔가 시간이 쫓기어 급하게 작업하시는 느낌이 들어요... 😭
아무도 @hyoseong1994 님께 빨리 개발하시라고 독촉하지 않습니다. 필요할 때는 천천히 학습을 하시는 기쁨도 좀 느끼시면서 프로젝트를 하셨으면 좋겠습니다. 여유를 좀 가지시고 제가 공유한 블로그 글이나 유튜브 영상을 한 번 보시기를 추천드립니다. 🙏
created_at: 2025-04-24T01:05:36Z
author_association: MEMBER
diff_hunk: @@ -1,19 +1,143 @@
-import { test } from "vitest";
+import { faker } from "@faker-js/faker";
+import { composeStories } from "@storybook/react";
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { expect, test, vi } from "vitest";
+import { fontSizes, fontWeights } from "../../tokens/typography";
+import * as stories from "./Link.stories";
 
-test.todo("renders children correctly");
+const { Basic, Tones, Contrasts, Underlines, Security, WithIcon } =
+  composeStories(stories);
 
-test.todo("applies the correct tone based on the 'tone' prop");
+test("renders the link with the correct text content", () => {
+  render(<Basic />);
+  expect(screen.getByRole("link")).toHaveTextContent("링크");
+});
 
-test.todo("applies the correct font size based on the 'size' prop");
+test("applies the correct tone based on the 'tone' prop", () => {
+  render(<Tones />);
 
-test.todo("applies the correct font weight based on the 'weight' prop");
+  expect(screen.getByRole("link", { name: "중립 링크" })).toHaveClass("c_text");
+  expect(screen.getByRole("link", { name: "강조 링크" })).toHaveClass(
+    "c_text.accent"
+  );
+  expect(screen.getByRole("link", { name: "위험 링크" })).toHaveClass(
+    "c_text.danger"
+  );
+  expect(screen.getByRole("link", { name: "경고 링크" })).toHaveClass(
+    "c_text.warning"
+  );
+});
 
-test.todo("renders with reduced contrast when 'muted' is true");
+test("applies the correct font size based on the 'size' prop", () => {
+  const size = faker.helpers.arrayElement(
+    Object.keys(fontSizes)
+  ) as keyof typeof fontSizes;
 
-test.todo("renders with underline when 'underline' is true");
+  render(<Basic size={size} />);
+  expect(screen.getByRole("link")).toHaveStyle({ fontSize: size });
+});
 
-test.todo("forwards additional anchor props like 'href' and 'target'");
+test("applies the correct font weight based on the 'weight' prop", () => {
+  const weight = faker.helpers.arrayElement(
+    Object.keys(fontWeights)
+  ) as keyof typeof fontWeights;
 
-test.todo("adds rel='noopener noreferrer' when target is '_blank'");
+  render(<Basic weight={weight} />);
+  expect(screen.getByRole("link")).toHaveClass(`fw_${weight}`);
+});
 
-test.todo("can be used with Icon component");
+test("applies the correct styles for low and high contrast", () => {
+  render(<Contrasts />);
+
+  expect(screen.getByRole("link", { name: "낮은 명암비" })).toHaveClass(
+    "c_text.muted"
+  );
+
+  expect(screen.getByRole("link", { name: "높은 명암비" })).toHaveClass(
+    "c_text"
+  );
+});
+
+test("applies the correct underline styles", () => {
+  render(<Underlines />);
+
+  expect(screen.getByRole("link", { name: "밑줄 있음" })).toHaveClass(
+    "td_underline"
+  );
+  expect(screen.getByRole("link", { name: "밑줄 없음" })).toHaveClass(
+    "td_none"
+  );
+});
+
+test("forwards additional anchor props like 'href' and 'target'", () => {
+  const href = faker.internet.url({ appendSlash: true });
+  const target = "_self";
+
+  render(<Basic href={href} target={target} />);
+
+  const link = screen.getByRole("link");
+  expect(link).toHaveAttribute("href", href);
+  expect(link).toHaveAttribute("target", target);
+});
+
+test("adds rel='noopener noreferrer' when target is '_blank'", () => {
+  render(<Security />);
+
+  expect(
+    screen.getByRole("link", { name: "새 탭에서 열기 (보안 속성 자동 추가)" })
+  ).toHaveAttribute("rel", "noopener noreferrer");
+
+  expect(
+    screen.getByRole("link", { name: "같은 탭에서 열기" })
+  ).not.toHaveAttribute("rel", "noopener noreferrer");
+});
+
+test("can be used with Icon component", () => {
+  render(<WithIcon />);
+
+  const link = screen.getByRole("link");
+  expect(link).toHaveTextContent("링크");
+  expect(link.querySelector("svg")).toBeInTheDocument();
+});
+
+test("navigates to the correct URL when clicked", async () => {
+  const href = faker.internet.url({ appendSlash: true });
+  render(<Basic href={href} onClick={undefined} />);
+
+  const link = screen.getByRole("link");
+  expect(link).toHaveAttribute("href", href);
+  await userEvent.click(link);
+
+  expect(window.location.href).toBe(href);
+});
+
+test("handles click events correctly", async () => {
+  const handleClick = vi.fn();
+  render(<Basic href="#" onClick={handleClick} />);
+
+  const link = screen.getByRole("link");
+  await userEvent.click(link);
+
+  expect(handleClick).toHaveBeenCalledTimes(1);
+});
+
+test("link is keyboard focusable", () => {
+  const href = faker.internet.url({ appendSlash: true });
+  render(<Basic href={href} />);
+
+  const link = screen.getByRole("link");
+  link.focus();
+  expect(link).toHaveFocus();
+});
+
+test("navigates when link is clicked with keyboard", async () => {
+  const href = faker.internet.url({ appendSlash: true });
+  render(<Basic href={href} onClick={undefined} />);
+
+  const link = screen.getByRole("link");
+  link.focus();
path: src/components/Link/Link.test.tsx
user: login: hyoseong1994
body: 넵 죄송합니다. 천천히 학습하는 마음으로 작업하도록하겠습니다.
공유한 내용 학습 후 작업 진행하도록하겠습니다.
created_at: 2025-04-24T01:08:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,19 +1,150 @@
-import { test } from "vitest";
+import { faker } from "@faker-js/faker";
+import { composeStories } from "@storybook/react";
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { describe, expect, test, vi } from "vitest";
+import { fontSizes, fontWeights } from "../../tokens/typography";
+import * as stories from "./Link.stories";
 
-test.todo("renders children correctly");
+const { Basic, Tones, Contrasts, Underlines, Security, WithIcon } =
+  composeStories(stories);
 
-test.todo("applies the correct tone based on the 'tone' prop");
+describe("rendering test", () => {
+  test("renders the link with the correct text content", () => {
+    render(<Basic />);
+    expect(screen.getByRole("link")).toHaveTextContent("링크");
+  });
 
-test.todo("applies the correct font size based on the 'size' prop");
+  test("can be used with Icon component", () => {
+    render(<WithIcon />);
 
-test.todo("applies the correct font weight based on the 'weight' prop");
+    const link = screen.getByRole("link");
+    expect(link).toHaveTextContent("링크");
+    expect(link.querySelector("svg")).toBeInTheDocument();
+  });
+});
 
-test.todo("renders with reduced contrast when 'muted' is true");
+describe("style test", () => {
+  test("applies the correct tone based on the 'tone' prop", () => {
+    render(<Tones />);
 
-test.todo("renders with underline when 'underline' is true");
+    expect(screen.getByRole("link", { name: "중립 링크" })).toHaveClass(
+      "c_text"
+    );
+    expect(screen.getByRole("link", { name: "강조 링크" })).toHaveClass(
+      "c_text.accent"
+    );
+    expect(screen.getByRole("link", { name: "위험 링크" })).toHaveClass(
+      "c_text.danger"
+    );
+    expect(screen.getByRole("link", { name: "경고 링크" })).toHaveClass(
+      "c_text.warning"
+    );
+  });
 
-test.todo("forwards additional anchor props like 'href' and 'target'");
+  test("applies the correct font size based on the 'size' prop", () => {
+    const size = faker.helpers.arrayElement(
+      Object.keys(fontSizes)
+    ) as keyof typeof fontSizes;
 
-test.todo("adds rel='noopener noreferrer' when target is '_blank'");
+    render(<Basic size={size} />);
+    expect(screen.getByRole("link")).toHaveStyle({ fontSize: size });
+  });
 
-test.todo("can be used with Icon component");
+  test("applies the correct font weight based on the 'weight' prop", () => {
+    const weight = faker.helpers.arrayElement(
+      Object.keys(fontWeights)
+    ) as keyof typeof fontWeights;
+
+    render(<Basic weight={weight} />);
+    expect(screen.getByRole("link")).toHaveClass(`fw_${weight}`);
+  });
+
+  test("applies the correct styles for low and high contrast", () => {
+    render(<Contrasts />);
+
+    expect(screen.getByRole("link", { name: "낮은 명암비" })).toHaveClass(
+      "c_text.muted"
+    );
+
+    expect(screen.getByRole("link", { name: "높은 명암비" })).toHaveClass(
+      "c_text"
+    );
+  });
+
+  test("applies the correct underline styles", () => {
+    render(<Underlines />);
+
+    expect(screen.getByRole("link", { name: "밑줄 있음" })).toHaveClass(
+      "td_underline"
+    );
+    expect(screen.getByRole("link", { name: "밑줄 없음" })).toHaveClass(
+      "td_none"
+    );
+  });
+});
+
+describe("behavior test", () => {
+  test("forwards additional anchor props like 'href' and 'target'", () => {
+    const href = faker.internet.url({ appendSlash: true });
+    const target = "_self";
+
+    render(<Basic href={href} target={target} />);
+
+    const link = screen.getByRole("link");
+    expect(link).toHaveAttribute("href", href);
+    expect(link).toHaveAttribute("target", target);
+  });
+
+  test("adds rel='noopener noreferrer' when target is '_blank'", () => {
+    render(<Security />);
+
+    expect(
+      screen.getByRole("link", { name: "새 탭에서 열기 (보안 속성 자동 추가)" })
+    ).toHaveAttribute("rel", "noopener noreferrer");
+
+    expect(
+      screen.getByRole("link", { name: "같은 탭에서 열기" })
+    ).not.toHaveAttribute("rel", "noopener noreferrer");
+  });
+
+  test("navigates to the correct URL when clicked", async () => {
+    const href = faker.internet.url({ appendSlash: true });
+    render(<Basic href={href} />);
+
+    const link = screen.getByRole("link");
+    expect(link).toHaveAttribute("href", href);
+    await userEvent.click(link);
+
+    expect(window.location.href).toBe(href);
+  });
+
+  test("handles click events correctly", async () => {
+    const handleClick = vi.fn();
+    render(<Basic href="#" onClick={handleClick} />);
+
+    const link = screen.getByRole("link");
+    await userEvent.click(link);
+
+    expect(handleClick).toHaveBeenCalledTimes(1);
+  });
+
+  test("link is keyboard focusable", async () => {
+    const href = faker.internet.url({ appendSlash: true });
+    render(<Basic href={href} />);
+
+    const link = screen.getByRole("link");
+    await userEvent.tab();
+    expect(link).toHaveFocus();
path: src/components/Link/Link.test.tsx
user: login: DaleSeo
body: nit: 사용자의 의식의 흐름 상 이 순서가 더 자연스럽지 않을까요?

```suggestion
    await userEvent.tab();
    
    const link = screen.getByRole("link");
    expect(link).toHaveFocus();
```
created_at: 2025-04-25T22:18:34Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,210 @@
+import type { Meta, StoryObj } from "@storybook/react";
+import { vstack } from "../../../styled-system/patterns";
+import { Link } from "./Link";
+import { Icon } from "../Icon/Icon";
+import { Text } from "../Text/Text";
+
+export default {
+  component: Link,
+  parameters: {
+    layout: "centered",
+  },
+  args: {
+    children: "링크",
+    href: "#",
+  },
+} satisfies Meta<typeof Link>;
+
+export const Basic: StoryObj<typeof Link> = {};
+
+export const Tones: StoryObj<typeof Link> = {
+  render: (args) => {
+    return (
+      <div className={vstack({ gap: "gap.md" })}>
+        <Link {...args} tone="neutral">
+          중립 링크
+        </Link>
+        <Link {...args} tone="accent">
+          강조 링크
+        </Link>
+        <Link {...args} tone="danger">
+          위험 링크
+        </Link>
+        <Link {...args} tone="warning">
+          경고 링크
+        </Link>
+      </div>
+    );
+  },
+  argTypes: {
+    children: {
+      control: false,
+    },
+    tone: {
+      control: false,
+    },
+  },
+};
+
+export const Contrasts: StoryObj<typeof Link> = {
+  render: (args) => {
+    return (
+      <div className={vstack({ gap: "gap.md" })}>
+        <Link {...args} muted>
+          낮은 명암비
+        </Link>
+        <Link {...args}>높은 명암비</Link>
+      </div>
+    );
+  },
+  argTypes: {
+    children: {
+      control: false,
+    },
+    muted: {
+      control: false,
+    },
+  },
+};
+
+export const Underlines: StoryObj<typeof Link> = {
+  render: (args) => {
+    return (
+      <div className={vstack({ gap: "gap.md" })}>
+        <Link {...args} underline>
+          밑줄 있음
+        </Link>
+        <Link {...args} underline={false}>
+          밑줄 없음
+        </Link>
+      </div>
+    );
+  },
+  argTypes: {
+    children: {
+      control: false,
+    },
+    underline: {
+      control: false,
+    },
+  },
+};
+
+export const Sizes: StoryObj<typeof Link> = {
+  render: (args) => {
+    return (
+      <div className={vstack({ gap: "gap.md" })}>
+        <Link {...args} size="sm">
+          작은 링크
+        </Link>
+        <Link {...args} size="md">
+          중간 링크
+        </Link>
+        <Link {...args} size="lg">
+          큰 링크
+        </Link>
+      </div>
+    );
+  },
+  argTypes: {
+    children: {
+      control: false,
+    },
+    size: {
+      control: false,
+    },
+  },
+};
+
+export const Weights: StoryObj<typeof Link> = {
+  render: (args) => {
+    return (
+      <div className={vstack({ gap: "gap.md" })}>
+        <Link {...args} weight="thin">
+          얇은 굵기
+        </Link>
+        <Link {...args} weight="light">
+          가벼운 굵기
+        </Link>
+        <Link {...args} weight="normal">
+          일반 굵기
+        </Link>
+        <Link {...args} weight="medium">
+          중간 굵기
+        </Link>
+        <Link {...args} weight="bold">
+          굵은 굵기
+        </Link>
+      </div>
+    );
+  },
+  argTypes: {
+    children: {
+      control: false,
+    },
+    weight: {
+      control: false,
+    },
+  },
+};
+
+export const Security: StoryObj<typeof Link> = {
+  render: (args) => {
+    return (
+      <div className={vstack({ gap: "gap.md" })}>
+        <Link {...args} target="_blank">
+          새 탭에서 열기 (보안 속성 자동 추가)
+        </Link>
+        <Link {...args} target="_self">
+          같은 탭에서 열기
+        </Link>
+      </div>
+    );
+  },
+  argTypes: {
+    children: {
+      control: false,
+    },
+    target: {
+      control: false,
+    },
+  },
+};
+
+export const Mismatch: StoryObj<typeof Link> = {
+  render: (args) => {
+    return (
+      <div className={vstack({ gap: "gap.md" })}>
+        <Link tone="accent" underline>
+          <Text tone="danger">나의 밑줄은 무슨 색일까? 🤔</Text>
+        </Link>
+        <Link size="sm" tone="accent" {...args}>
+          <Icon name="clock" size="xl" tone="danger" />
+          링크 아이콘
+        </Link>
+        <Link tone="danger"></Link>
+      </div>
+    );
+  },
+};
path: src/components/Link/Link.stories.tsx
user: login: DaleSeo
body: 이 스토리는 사용자에게 오히려 링크 컴포넌트를 우리가 의도하지 않은 방향으로 쓰도록 힌트를 주는 역효과가 있을 것 같습니다 😅
created_at: 2025-04-25T22:25:21Z
author_association: MEMBER
diff_hunk: @@ -1,19 +1,150 @@
-import { test } from "vitest";
+import { faker } from "@faker-js/faker";
+import { composeStories } from "@storybook/react";
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { describe, expect, test, vi } from "vitest";
+import { fontSizes, fontWeights } from "../../tokens/typography";
+import * as stories from "./Link.stories";
 
-test.todo("renders children correctly");
+const { Basic, Tones, Contrasts, Underlines, Security, WithIcon } =
+  composeStories(stories);
 
-test.todo("applies the correct tone based on the 'tone' prop");
+describe("rendering test", () => {
+  test("renders the link with the correct text content", () => {
+    render(<Basic />);
+    expect(screen.getByRole("link")).toHaveTextContent("링크");
+  });
 
-test.todo("applies the correct font size based on the 'size' prop");
+  test("can be used with Icon component", () => {
+    render(<WithIcon />);
 
-test.todo("applies the correct font weight based on the 'weight' prop");
+    const link = screen.getByRole("link");
+    expect(link).toHaveTextContent("링크");
+    expect(link.querySelector("svg")).toBeInTheDocument();
+  });
+});
 
-test.todo("renders with reduced contrast when 'muted' is true");
+describe("style test", () => {
+  test("applies the correct tone based on the 'tone' prop", () => {
+    render(<Tones />);
 
-test.todo("renders with underline when 'underline' is true");
+    expect(screen.getByRole("link", { name: "중립 링크" })).toHaveClass(
+      "c_text"
+    );
+    expect(screen.getByRole("link", { name: "강조 링크" })).toHaveClass(
+      "c_text.accent"
+    );
+    expect(screen.getByRole("link", { name: "위험 링크" })).toHaveClass(
+      "c_text.danger"
+    );
+    expect(screen.getByRole("link", { name: "경고 링크" })).toHaveClass(
+      "c_text.warning"
+    );
+  });
 
-test.todo("forwards additional anchor props like 'href' and 'target'");
+  test("applies the correct font size based on the 'size' prop", () => {
+    const size = faker.helpers.arrayElement(
+      Object.keys(fontSizes)
+    ) as keyof typeof fontSizes;
 
-test.todo("adds rel='noopener noreferrer' when target is '_blank'");
+    render(<Basic size={size} />);
+    expect(screen.getByRole("link")).toHaveStyle({ fontSize: size });
+  });
 
-test.todo("can be used with Icon component");
+  test("applies the correct font weight based on the 'weight' prop", () => {
+    const weight = faker.helpers.arrayElement(
+      Object.keys(fontWeights)
+    ) as keyof typeof fontWeights;
+
+    render(<Basic weight={weight} />);
+    expect(screen.getByRole("link")).toHaveClass(`fw_${weight}`);
+  });
+
+  test("applies the correct styles for low and high contrast", () => {
+    render(<Contrasts />);
+
+    expect(screen.getByRole("link", { name: "낮은 명암비" })).toHaveClass(
+      "c_text.muted"
+    );
+
+    expect(screen.getByRole("link", { name: "높은 명암비" })).toHaveClass(
+      "c_text"
+    );
+  });
+
+  test("applies the correct underline styles", () => {
+    render(<Underlines />);
+
+    expect(screen.getByRole("link", { name: "밑줄 있음" })).toHaveClass(
+      "td_underline"
+    );
+    expect(screen.getByRole("link", { name: "밑줄 없음" })).toHaveClass(
+      "td_none"
+    );
+  });
+});
+
+describe("behavior test", () => {
+  test("forwards additional anchor props like 'href' and 'target'", () => {
+    const href = faker.internet.url({ appendSlash: true });
+    const target = "_self";
+
+    render(<Basic href={href} target={target} />);
+
+    const link = screen.getByRole("link");
+    expect(link).toHaveAttribute("href", href);
+    expect(link).toHaveAttribute("target", target);
+  });
+
+  test("adds rel='noopener noreferrer' when target is '_blank'", () => {
+    render(<Security />);
+
+    expect(
+      screen.getByRole("link", { name: "새 탭에서 열기 (보안 속성 자동 추가)" })
+    ).toHaveAttribute("rel", "noopener noreferrer");
+
+    expect(
+      screen.getByRole("link", { name: "같은 탭에서 열기" })
+    ).not.toHaveAttribute("rel", "noopener noreferrer");
+  });
+
+  test("navigates to the correct URL when clicked", async () => {
+    const href = faker.internet.url({ appendSlash: true });
+    render(<Basic href={href} />);
+
+    const link = screen.getByRole("link");
+    expect(link).toHaveAttribute("href", href);
+    await userEvent.click(link);
+
+    expect(window.location.href).toBe(href);
+  });
+
+  test("handles click events correctly", async () => {
+    const handleClick = vi.fn();
+    render(<Basic href="#" onClick={handleClick} />);
+
+    const link = screen.getByRole("link");
+    await userEvent.click(link);
+
+    expect(handleClick).toHaveBeenCalledTimes(1);
+  });
+
+  test("link is keyboard focusable", async () => {
+    const href = faker.internet.url({ appendSlash: true });
+    render(<Basic href={href} />);
+
+    const link = screen.getByRole("link");
+    await userEvent.tab();
+    expect(link).toHaveFocus();
path: src/components/Link/Link.test.tsx
user: login: hyoseong1994
body: 맞습니다. 사용자 관점에서 작성해야하는데 무의식적으로 변수를 위로 올려야한다고 생각한거 같습니다. 수정하도록 하겠습니다.
created_at: 2025-04-25T22:40:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,210 @@
+import type { Meta, StoryObj } from "@storybook/react";
+import { vstack } from "../../../styled-system/patterns";
+import { Link } from "./Link";
+import { Icon } from "../Icon/Icon";
+import { Text } from "../Text/Text";
+
+export default {
+  component: Link,
+  parameters: {
+    layout: "centered",
+  },
+  args: {
+    children: "링크",
+    href: "#",
+  },
+} satisfies Meta<typeof Link>;
+
+export const Basic: StoryObj<typeof Link> = {};
+
+export const Tones: StoryObj<typeof Link> = {
+  render: (args) => {
+    return (
+      <div className={vstack({ gap: "gap.md" })}>
+        <Link {...args} tone="neutral">
+          중립 링크
+        </Link>
+        <Link {...args} tone="accent">
+          강조 링크
+        </Link>
+        <Link {...args} tone="danger">
+          위험 링크
+        </Link>
+        <Link {...args} tone="warning">
+          경고 링크
+        </Link>
+      </div>
+    );
+  },
+  argTypes: {
+    children: {
+      control: false,
+    },
+    tone: {
+      control: false,
+    },
+  },
+};
+
+export const Contrasts: StoryObj<typeof Link> = {
+  render: (args) => {
+    return (
+      <div className={vstack({ gap: "gap.md" })}>
+        <Link {...args} muted>
+          낮은 명암비
+        </Link>
+        <Link {...args}>높은 명암비</Link>
+      </div>
+    );
+  },
+  argTypes: {
+    children: {
+      control: false,
+    },
+    muted: {
+      control: false,
+    },
+  },
+};
+
+export const Underlines: StoryObj<typeof Link> = {
+  render: (args) => {
+    return (
+      <div className={vstack({ gap: "gap.md" })}>
+        <Link {...args} underline>
+          밑줄 있음
+        </Link>
+        <Link {...args} underline={false}>
+          밑줄 없음
+        </Link>
+      </div>
+    );
+  },
+  argTypes: {
+    children: {
+      control: false,
+    },
+    underline: {
+      control: false,
+    },
+  },
+};
+
+export const Sizes: StoryObj<typeof Link> = {
+  render: (args) => {
+    return (
+      <div className={vstack({ gap: "gap.md" })}>
+        <Link {...args} size="sm">
+          작은 링크
+        </Link>
+        <Link {...args} size="md">
+          중간 링크
+        </Link>
+        <Link {...args} size="lg">
+          큰 링크
+        </Link>
+      </div>
+    );
+  },
+  argTypes: {
+    children: {
+      control: false,
+    },
+    size: {
+      control: false,
+    },
+  },
+};
+
+export const Weights: StoryObj<typeof Link> = {
+  render: (args) => {
+    return (
+      <div className={vstack({ gap: "gap.md" })}>
+        <Link {...args} weight="thin">
+          얇은 굵기
+        </Link>
+        <Link {...args} weight="light">
+          가벼운 굵기
+        </Link>
+        <Link {...args} weight="normal">
+          일반 굵기
+        </Link>
+        <Link {...args} weight="medium">
+          중간 굵기
+        </Link>
+        <Link {...args} weight="bold">
+          굵은 굵기
+        </Link>
+      </div>
+    );
+  },
+  argTypes: {
+    children: {
+      control: false,
+    },
+    weight: {
+      control: false,
+    },
+  },
+};
+
+export const Security: StoryObj<typeof Link> = {
+  render: (args) => {
+    return (
+      <div className={vstack({ gap: "gap.md" })}>
+        <Link {...args} target="_blank">
+          새 탭에서 열기 (보안 속성 자동 추가)
+        </Link>
+        <Link {...args} target="_self">
+          같은 탭에서 열기
+        </Link>
+      </div>
+    );
+  },
+  argTypes: {
+    children: {
+      control: false,
+    },
+    target: {
+      control: false,
+    },
+  },
+};
+
+export const Mismatch: StoryObj<typeof Link> = {
+  render: (args) => {
+    return (
+      <div className={vstack({ gap: "gap.md" })}>
+        <Link tone="accent" underline>
+          <Text tone="danger">나의 밑줄은 무슨 색일까? 🤔</Text>
+        </Link>
+        <Link size="sm" tone="accent" {...args}>
+          <Icon name="clock" size="xl" tone="danger" />
+          링크 아이콘
+        </Link>
+        <Link tone="danger"></Link>
+      </div>
+    );
+  },
+};
path: src/components/Link/Link.stories.tsx
user: login: hyoseong1994
body: 회의때 어떤 방식으로 진행하면 좋을지에 관해서 예시로 사용하려고 남겨두었는데 달레님 말씀대로 오히려 역효과가 날거 같군요 제거하고 해당 내용은 스크린샷으로 티켓에 남겨두도록하겠습니다.
created_at: 2025-04-25T22:41:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -15,6 +16,159 @@ interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
   underline?: boolean;
 }
 
-export function Link({ children }: PropsWithChildren<LinkProps>) {
-  return <>{children}</>;
+/**
+ * - `underline` 속성으로 밑줄 표시 여부를 설정할 수 있습니다.
+ * - `target="_blank"`를 사용할 때는 자동으로 보안 속성이 추가됩니다.
+ * - `tone` 속성을 통해서 링크의 색상을 변경할 수 있습니다.
+ * - `size` 속성과 `weight` 속성을 통해서 텍스트 스타일을 변경할 수 있습니다.
+ * - `muted` 속성을 주시면 글자색이 옅어집니다. 명암비가 낮아지므로 접근성 측면에서 주의해서 사용하세요.
+ */
+export function Link({
+  children,
+  tone,
+  size,
+  weight,
+  muted,
+  underline,
+  target,
+  rel,
+  ...props
+}: PropsWithChildren<LinkProps>) {
+  return (
+    <a
+      className={css(
+        styles.raw({ tone, muted, underline }),
+        css.raw({
+          fontSize: size,
path: src/components/Link/Link.tsx
user: login: Sunjae95
body: ### IMO
저도 Link 컴포넌트에서는 children을 순회하는건 좋지않은 방법이라 생각했어요.
하지만 사이즈를 조정하기 위해서는 children을 순회해야한다라는 생각에서부터 컴파운드 패턴까지 도달하게 됐어요.

```tsx
// 1. 기본 스타일 적용
<Link {...props} >hello</Link>

// Link.Icon을 사용하지 한다면 -> Link를 내부에서 정의한 Icon 스타일 적용
<Link>
  <Link.Icon {...props} />
  <Link.Text>hello</Link.Text>
</Link>

// Link.Text을 사용하지 않는다면 -> text 스타일 Link에 속한 것으로 적용
<Link>
  <Link.Icon {...props} />
  hello
</Link>

// Link.Icon을 사용하지 않는다면 -> Icon 컴포넌트의 스타일을 따름
<Link>
  <Icon {...props} />
  <Link.Text>hello</Link.Text>
</Link>
```


created_at: 2025-04-29T01:38:09Z
author_association: MEMBER
diff_hunk: @@ -15,6 +16,159 @@ interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
   underline?: boolean;
 }
 
-export function Link({ children }: PropsWithChildren<LinkProps>) {
-  return <>{children}</>;
+/**
+ * - `underline` 속성으로 밑줄 표시 여부를 설정할 수 있습니다.
+ * - `target="_blank"`를 사용할 때는 자동으로 보안 속성이 추가됩니다.
+ * - `tone` 속성을 통해서 링크의 색상을 변경할 수 있습니다.
+ * - `size` 속성과 `weight` 속성을 통해서 텍스트 스타일을 변경할 수 있습니다.
+ * - `muted` 속성을 주시면 글자색이 옅어집니다. 명암비가 낮아지므로 접근성 측면에서 주의해서 사용하세요.
+ */
path: src/components/Link/Link.tsx
user: login: RiaOh
body: 오.. 이렇게 주석을 작성하면 Docs에도 반영이 되는군요. 하나 배워갑니다 :) 감사합니다!
created_at: 2025-05-03T08:49:06Z
author_association: CONTRIBUTOR

PR comments:
user: login: DaleSeo
created_at: 2025-04-17T23:54:18Z
body: 현재 PR에 main 브랜치의 최신 상태가 반영되어 있는 걸까요? PR #133 에서 다크모드 버그 수정한 게 작동을 안 하고 있는 것 같아서요. (cc: @Sunjae95)

https://github.com/user-attachments/assets/02310344-5bb9-49c7-b762-e6394a451454
author_association: MEMBER
user: login: sounmind
created_at: 2025-04-18T12:25:54Z
body: PR 제목을 한국어 사용자가 읽기 편하도록 한국어로 작성하시면 어떨까요?
author_association: MEMBER
user: login: Sunjae95
created_at: 2025-04-18T13:16:14Z
body: > 현재 PR에 main 브랜치의 최신 상태가 반영되어 있는 걸까요? PR #133 에서 다크모드 버그 수정한 게 작동을 안 하고 있는 것 같아서요. (cc: @Sunjae95)
> 
>  Shot.2025-04-17.at.19.52.08.mp4

네 최신 main기준이 아니라서 적용이 안되있는거네요. 
@hyoseong1994  나중에 merge할 때 최신 main기준으로 부탁드립니다
author_association: MEMBER
user: login: DaleSeo
created_at: 2025-04-22T00:40:26Z
body: > 저는 별도의 이견은 없습니다.
> 달레님 코멘트 부분 반영되면 다시 확인해볼게요!
> LGTM! 🙂

@Sunjae95 님, 이렇게 제가 검사하는 분위기 조성하시면 불편합니다 ㅋㅋ
author_association: MEMBER
user: login: Sunjae95
created_at: 2025-04-24T00:15:58Z
body: > > 저는 별도의 이견은 없습니다.
> > 달레님 코멘트 부분 반영되면 다시 확인해볼게요!
> > LGTM! 🙂
> 
> @Sunjae95 님, 이렇게 제가 검사하는 분위기 조성하시면 불편합니다 ㅋㅋ

헉.. 그렇게 보일수도 있군요..! 😅 
author_association: MEMBER
user: login: hyoseong1994
created_at: 2025-04-29T00:25:00Z
body: @sounmind @Sunjae95 
안녕하세요 추가적인 리뷰 없으시다면 머지하려고합니다. 
추가적으로 리뷰 하실 부분 있을까요?
author_association: CONTRIBUTOR