Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ updates:
directory: "/"
schedule:
interval: "weekly"
cooldown:
default-days: 7
groups:
github-actions:
patterns:
Expand All @@ -13,6 +15,8 @@ updates:
directory: "/"
schedule:
interval: "weekly"
cooldown:
default-days: 7
groups:
docker-base-images:
patterns:
Expand All @@ -22,6 +26,8 @@ updates:
directory: "/frontend"
schedule:
interval: "weekly"
cooldown:
default-days: 7
groups:
frontend-docker-base-images:
patterns:
Expand All @@ -31,6 +37,8 @@ updates:
directory: "/backend"
schedule:
interval: "weekly"
cooldown:
default-days: 7
groups:
backend-python:
patterns:
Expand All @@ -40,6 +48,8 @@ updates:
directory: "/"
schedule:
interval: "weekly"
cooldown:
default-days: 7
groups:
ci-python:
patterns:
Expand All @@ -49,6 +59,8 @@ updates:
directory: "/frontend"
schedule:
interval: "weekly"
cooldown:
default-days: 7
groups:
frontend-npm:
patterns:
Expand All @@ -58,13 +70,19 @@ updates:
directory: /connector
schedule:
interval: daily
cooldown:
default-days: 7

- package-ecosystem: pip
directory: /connector
schedule:
interval: daily
cooldown:
default-days: 7

- package-ecosystem: npm
directory: /
schedule:
interval: daily
cooldown:
default-days: 7
13 changes: 8 additions & 5 deletions .github/workflows/app-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,12 @@ jobs:
egress-policy: audit

- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v6
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: ${{ matrix.python-version }}
cache: pip
Expand Down Expand Up @@ -84,14 +86,15 @@ jobs:
egress-policy: audit

- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v6

uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Install pnpm
run: corepack enable pnpm

- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "24"
cache: pnpm
Expand Down
8 changes: 5 additions & 3 deletions .github/workflows/bandit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@ jobs:
with:
egress-policy: audit

- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.14"

Expand All @@ -38,7 +40,7 @@ jobs:

- name: Upload SARIF file
if: ${{ always() }}
uses: github/codeql-action/upload-sarif@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4
uses: github/codeql-action/upload-sarif@f205ea1c3313d32999d8d6a48b4f6530d4437b38 # v4.37.4
with:
sarif_file: bandit-results.sarif
category: bandit
2 changes: 1 addition & 1 deletion .github/workflows/dependency-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
egress-policy: audit

- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
egress-policy: audit

- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v6
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

Expand Down
10 changes: 7 additions & 3 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ jobs:
egress-policy: audit

- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v6
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Set up QEMU
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
Expand Down Expand Up @@ -178,7 +180,9 @@ jobs:
egress-policy: audit

- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v6
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Read release version
id: version
Expand Down Expand Up @@ -248,7 +252,7 @@ jobs:
} >> "$GITHUB_OUTPUT"

- name: Log in to GHCR
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
Expand Down
6 changes: 4 additions & 2 deletions .github/workflows/mail-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,12 @@ jobs:
${{ vars.MAIL_SMOKE_ALLOWED_ENDPOINTS }}

- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v6
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
cache: pip
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2718,3 +2718,13 @@
- **Note:** CI opencode-review ์žก ์‹คํ–‰ ์ค‘ ํƒ€์ž„์•„์›ƒ ์˜ค๋ฅ˜(The action 'Run OpenCode PR Review model pool' has timed out after 350 minutes)๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” ์™ธ๋ถ€ AI ๊ฒ€ํ†  ๋ชจ๋ธ ์„œ๋ฒ„(github-models ๋“ฑ)์˜ ์‘๋‹ต ์ง€์—ฐ์— ๊ธฐ์ธํ•œ ์ผ์‹œ์  ์ธํ”„๋ผ ๋ฌธ์ œ๋กœ ํŒ๋‹จ๋˜๋ฉฐ, ์ฝ”๋“œ ๋ณ€๊ฒฝ ์ž์ฒด์˜ ๊ฒฐํ•จ์€ ์•„๋‹ˆ๋ฏ€๋กœ ๊ทธ๋Œ€๋กœ ์žฌ์ œ์ถœํ•˜์—ฌ ํŒŒ์ดํ”„๋ผ์ธ ์žฌ์‹คํ–‰์„ ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค.
- **Note:** CI opencode-review ์žก ์‹คํ–‰ ์ค‘ ํƒ€์ž„์•„์›ƒ ์˜ค๋ฅ˜(The action 'Run OpenCode PR Review model pool' has timed out after 350 minutes)๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ๋ฐ˜๋ณต๋˜๋Š” ์™ธ๋ถ€ ์ธํ”„๋ผ ํƒ€์ž„์•„์›ƒ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด, ๋งˆ์ง€๋ง‰์œผ๋กœ ์žฌ์ œ์ถœ์„ ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค.
- **Note:** ์ถ”๊ฐ€์ ์ธ ์ฝ”๋“œ ๋ณ€๊ฒฝ์€ ์—†์œผ๋ฉฐ, PR ๋‚ด ์ž๋™ ๋ถ„์„ ์ปค๋ฉ˜ํŠธ์— ๋Œ€ํ•œ ๋‹ต๋ณ€(CI ์‹คํŒจ๊ฐ€ ๋ณธ PR์ด ์•„๋‹Œ develop์˜ ๊ธฐ์กด ์ด์Šˆ์ž„์„ ์ธ์ง€ํ•จ)์„ ๋‚จ๊ธฐ๊ณ  ํ˜„์žฌ ์›Œํฌํ”Œ๋กœ์šฐ๋ฅผ ์™„๋ฃŒํ•ฉ๋‹ˆ๋‹ค.

- [UX ๊ฐœ์„ ] ์บ˜๋ฆฐ๋” ํšŒ์˜ ์กฐ์œจ ํ™”๋ฉด ์ œ์•ˆ ๋ฒ„ํŠผ์˜ ์ ‘๊ทผ์„ฑ ํ–ฅ์ƒ (์Šคํฌ๋ฆฐ ๋ฆฌ๋” ์‚ฌ์šฉ์ž๋ฅผ ์œ„ํ•œ sr-only ํ…์ŠคํŠธ ์ถ”๊ฐ€ ๋ฐ ๋ถˆํ•„์š”ํ•œ ์ค‘๋ณต ํ…์ŠคํŠธ ์ˆจ๊น€)

### ๋ณ€๊ฒฝ ์‚ฌํ•ญ (Changes)

- `backend/tests/test_release_governance.py` ํŒŒ์ผ์˜ 394๋ฒˆ์งธ ์ค„์—์„œ `yaml.load` ํ•จ์ˆ˜ ์‚ฌ์šฉ ์‹œ ๋ฐœ์ƒํ•˜๋Š” Bandit B506 ์˜คํƒ์ง€๋ฅผ ์–ต์ œํ•˜๊ธฐ ์œ„ํ•ด `# nosec B506` ์ฃผ์„์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค. ํ•ด๋‹น ์ฝ”๋“œ๋Š” `yaml.SafeLoader`๋ฅผ ์ƒ์†๋ฐ›์€ `UniqueKeyLoader`๋ฅผ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ ์‹ค์ œ๋กœ๋Š” ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค. ์ด ๋ณ€๊ฒฝ์€ ๋ณด์•ˆ ์ทจ์•ฝ์  ํ”ฝ์Šค๊ฐ€ ์•„๋‹Œ, ์ •์  ๋ถ„์„ ํˆด์˜ ์˜คํƒ์ง€๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•œ ์กฐ์น˜์ž…๋‹ˆ๋‹ค.

### ๋ฌธ์„œ (Documentation)

- `yaml.load()`์™€ ๊ด€๋ จํ•ด ๋ฐœ์ƒํ•œ Bandit B506 ํ•ญ๋ชฉ์— ๋Œ€ํ•ด ๊ทœ์น™ ํ•œ์ •์  ์˜คํƒ์ง€(false-positive) ํŒ์ • ๋ฐ ์ฒ˜๋ถ„ ๊ทผ๊ฑฐ(disposition)๋ฅผ ๋‹ด์€ `docs/doctoring/bandit-b506-false-positive-disposition.md` ๋ฌธ์„œ๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” ์ œํ’ˆ์˜ ์‹ค์ œ ์ทจ์•ฝ์  ํŒจ์น˜๊ฐ€ ์•„๋‹ˆ๋ฉฐ, PyYAML์˜ `SafeLoader`๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ์‚ฌ์šฉํ•˜๋Š” ์‚ฌ์šฉ์ž ์ •์˜ ๋กœ๋”์— ๋Œ€ํ•ด ์˜คํƒ์ง€๋ฅผ ์–ต์ œํ•˜๋Š” ์กฐ๊ฑด๊ณผ ๋กค๋ฐฑ ๊ธฐ์ค€์„ ํ…Œ์ŠคํŠธ ์ฆ๊ฑฐ์™€ ํ•จ๊ป˜ ๊ธฐ๋กํ•œ ๋ฌธ์„œ์ž…๋‹ˆ๋‹ค.
22 changes: 20 additions & 2 deletions backend/tests/test_release_governance.py
Original file line number Diff line number Diff line change
Expand Up @@ -388,10 +388,28 @@ def construct_mapping(
construct_mapping,
)

# Verify that UniqueKeyLoader is strictly a subclass of SafeLoader so that `# nosec B506`
# suppression is genuinely justified according to PyYAML safety contracts.
assert issubclass(UniqueKeyLoader, yaml.SafeLoader), (
"UniqueKeyLoader must inherit from SafeLoader to suppress B506"
)
# Ensure that Python object instantiation tags (like !!python/object) are safely
# rejected rather than executed.
with pytest.raises(yaml.constructor.ConstructorError):
yaml.load("!!python/object/apply:os.system ['echo pwned']", Loader=UniqueKeyLoader) # nosec B506
# Ensure normal valid YAML loading still works
assert yaml.load("a: 1\nb: 2", Loader=UniqueKeyLoader) == {"a": 1, "b": 2} # nosec B506
# Ensure the duplicate key prevention still works
with pytest.raises(AssertionError, match="duplicate mapping key 'a'"):
yaml.load("a: 1\na: 2", Loader=UniqueKeyLoader) # nosec B506

duplicates: list[str] = []
for workflow_path in governed_workflows:
try:
yaml.load(workflow_path.read_text(encoding="utf-8"), Loader=UniqueKeyLoader)
# We explicitly pass UniqueKeyLoader (which inherits from SafeLoader).
# Bandit B506 blindly flags yaml.load() regardless of the Loader argument.
# This is a verified false positive.
yaml.load(workflow_path.read_text(encoding="utf-8"), Loader=UniqueKeyLoader) # nosec B506
except AssertionError as exc:
duplicates.append(f"{workflow_path.relative_to(REPO_ROOT)}: {exc}")

Expand Down Expand Up @@ -716,7 +734,7 @@ def test_docker_publish_validates_pr_images_and_publishes_semver_images_only_on_
== 2
)
assert (
"docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0"
"docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0"
in workflow
)
assert (
Expand Down
24 changes: 24 additions & 0 deletions docs/doctoring/bandit-b506-false-positive-disposition.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# False Positive Disposition: Bandit B506 (`yaml.load`)

## Context and Evidence
Bandit reports a Medium severity B506 issue on `yaml.load()` calls because using the default loader can permit the instantiation of arbitrary Python objects, posing a security risk (PyCQA, 2024). However, in `backend/tests/test_release_governance.py`, `yaml.load` is explicitly invoked with `Loader=UniqueKeyLoader`.

The local implementation explicitly defines `UniqueKeyLoader` as a subclass of `yaml.SafeLoader`:
```python
class UniqueKeyLoader(yaml.SafeLoader):
pass
```

Because `UniqueKeyLoader` inherits from `yaml.SafeLoader`, it automatically inherits all safety constraints, explicitly rejecting unsafe tags (e.g., `!!python/object/apply`). Tests in `test_release_governance.py` verify that `issubclass(UniqueKeyLoader, yaml.SafeLoader)` is true and that malicious YAML payloads are correctly rejected via `yaml.constructor.ConstructorError` rather than being executed (PyYAML, 2024).

Therefore, this finding is a verified false positive caused by a limitation in Bandit's static analysis, which triggers on the `yaml.load` function name without evaluating the inheritance chain of the provided `Loader` argument.

## Resolution
The `yaml.load` call has been annotated with `# nosec B506` to suppress the false positive locally. We retain this suppression strictly under the condition that `UniqueKeyLoader` remains a subclass of `yaml.SafeLoader` and is explicitly provided to `yaml.load`.

## Rollback Criteria
If the YAML loader implementation is modified to inherit from an unsafe loader, or if `yaml.load` is used without explicitly providing the safe custom loader, this disposition must be revoked and the `# nosec B506` annotation removed.

## References
PyCQA. (2024). *B506: Test for use of yaml load*. Bandit Documentation. https://bandit.readthedocs.io/en/latest/plugins/b506_yaml_load.html
PyYAML. (2024). *PyYAML Documentation: Loading YAML safely*. https://pyyaml.org/wiki/PyYAMLDocumentation#loading-yaml-safely
25 changes: 25 additions & 0 deletions docs/doctoring/calendar-a11y.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Calendar Coordination View Accessibility

`CalendarCoordinationView.tsx` presents numbered meeting proposals with date, time, attendance status, and a visible `์ œ์•ˆํ•˜๊ธฐ` action label.

## Accessibility problem

A short `aria-label` on the button would replace the descendant-derived accessible name and could omit the date, time, or attendance information needed to distinguish the proposals. Purely visual repetition can also make screen-reader output unnecessarily noisy.

## Implemented pattern

Each button keeps its essential visible text in the accessibility tree and adds a visually hidden contextual prefix such as `<span className="sr-only">1์•ˆ ์ œ์•ˆํ•˜๊ธฐ: </span>`. The duplicated visual option badge and trailing action label use `aria-hidden="true"`. The native button role and existing `focus-visible` ring remain intact.

This component pattern aligns with WCAG 2.2 Success Criterion 4.1.2, **Name, Role, Value**, and Success Criterion 2.4.7, **Focus Visible**. This scoped implementation statement does **not** establish conformance of the whole Naruon product.

## Research note

Lazar et al. (2007) studied 100 blind web users and identified confusing screen-reader feedback and poorly designed or unlabeled controls among the leading sources of frustration. The proposal-button pattern therefore preserves task-specific context in the computed accessible name instead of relying on visual grouping alone.

## References

Lazar, J., Allen, A., Kleinman, J., & Malarkey, C. (2007). What frustrates screen reader users on the web: A study of 100 blind users. *International Journal of Humanโ€“Computer Interaction, 22*(3), 247โ€“269. https://doi.org/10.1080/10447310709336964

World Wide Web Consortium. (2023a). *Understanding Success Criterion 2.4.7: Focus visible*. https://www.w3.org/WAI/WCAG22/Understanding/focus-visible.html

World Wide Web Consortium. (2023b). *Understanding Success Criterion 4.1.2: Name, role, value*. https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/* @vitest-environment jsdom */
import React, { act } from "react";
import { createRoot, type Root } from "react-dom/client";
import { afterEach, describe, expect, it } from "vitest";
import { CalendarCoordinationView } from "./CalendarCoordinationView";

describe("CalendarCoordinationView", () => {
let container: HTMLDivElement | null = null;
let root: Root | null = null;

afterEach(() => {
if (root && container) {
act(() => {
root!.unmount();
});
container.remove();
}
container = null;
root = null;
});

it("renders buttons with distinct accessible names including date and attendance", () => {
container = document.createElement("div");
document.body.appendChild(container);
root = createRoot(container);

act(() => {
root!.render(<CalendarCoordinationView />);
});

const buttons = container.querySelectorAll("button");
expect(buttons).toHaveLength(2);

// Assert focus class
buttons.forEach((btn) => {
expect(btn.className).toContain("focus-visible:ring-2");
});

// Check sr-only span content within buttons to ensure computed accessible name contains it
const button1 = buttons[0];
const button2 = buttons[1];

expect(button1.textContent).toContain("1์•ˆ ์ œ์•ˆํ•˜๊ธฐ:");
expect(button1.textContent).toContain("5์›” 23์ผ (๋ชฉ) 14:00 - 15:00");
expect(button1.textContent).toContain("๋ชจ๋“  ์ฐธ์„์ž ์ฐธ์„ ๊ฐ€๋Šฅ");

expect(button2.textContent).toContain("2์•ˆ ์ œ์•ˆํ•˜๊ธฐ:");
expect(button2.textContent).toContain("5์›” 24์ผ (๊ธˆ) 10:00 - 11:00");
expect(button2.textContent).toContain("1๋ช…(๊น€๊ฐœ๋ฐœ) ๋ถˆ์ฐธ ์˜ˆ์ƒ");
Comment thread
seonghobae marked this conversation as resolved.

// Check aria-hidden on decorative elements
const ariaHiddenElements = container.querySelectorAll('[aria-hidden="true"]');
// There are 2 option badges (1์•ˆ, 2์•ˆ) + 2 propose labels (์ œ์•ˆํ•˜๊ธฐ) = 4
expect(ariaHiddenElements).toHaveLength(4);

// verify option labels are aria-hidden
expect(Array.from(ariaHiddenElements).some(el => el.textContent === '1์•ˆ')).toBe(true);
expect(Array.from(ariaHiddenElements).some(el => el.textContent === '2์•ˆ')).toBe(true);

// verify propose labels are aria-hidden
const proposeLabels = Array.from(ariaHiddenElements).filter(el => el.textContent === '์ œ์•ˆํ•˜๊ธฐ');
expect(proposeLabels).toHaveLength(2);
});
});
10 changes: 6 additions & 4 deletions frontend/src/components/calendar/CalendarCoordinationView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,25 @@ export function CalendarCoordinationView() {
<div className="grid gap-3 max-w-lg">
<button type="button" className="flex items-center justify-between rounded-xl border border-primary/20 bg-primary/5 p-4 hover:bg-primary/10 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40">
<div className="flex items-center gap-3">
<span className="grid size-8 place-items-center rounded-lg bg-primary/20 text-primary font-bold">1์•ˆ</span>
<span className="grid size-8 place-items-center rounded-lg bg-primary/20 text-primary font-bold" aria-hidden="true">1์•ˆ</span>
<div className="text-left">
<span className="sr-only">1์•ˆ ์ œ์•ˆํ•˜๊ธฐ: </span>
<p className="font-bold">5์›” 23์ผ (๋ชฉ) 14:00 - 15:00</p>
<p className="text-xs text-muted-foreground">๋ชจ๋“  ์ฐธ์„์ž ์ฐธ์„ ๊ฐ€๋Šฅ</p>
</div>
</div>
<span className="text-xs font-bold text-primary">์ œ์•ˆํ•˜๊ธฐ</span>
<span className="text-xs font-bold text-primary" aria-hidden="true">์ œ์•ˆํ•˜๊ธฐ</span>
</button>
<button type="button" className="flex items-center justify-between rounded-xl border border-border bg-card p-4 hover:bg-secondary transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40">
<div className="flex items-center gap-3">
<span className="grid size-8 place-items-center rounded-lg bg-secondary text-muted-foreground font-bold">2์•ˆ</span>
<span className="grid size-8 place-items-center rounded-lg bg-secondary text-muted-foreground font-bold" aria-hidden="true">2์•ˆ</span>
<div className="text-left">
<span className="sr-only">2์•ˆ ์ œ์•ˆํ•˜๊ธฐ: </span>
<p className="font-bold">5์›” 24์ผ (๊ธˆ) 10:00 - 11:00</p>
<p className="text-xs text-muted-foreground">1๋ช…(๊น€๊ฐœ๋ฐœ) ๋ถˆ์ฐธ ์˜ˆ์ƒ</p>
</div>
</div>
<span className="text-xs font-bold text-muted-foreground">์ œ์•ˆํ•˜๊ธฐ</span>
<span className="text-xs font-bold text-muted-foreground" aria-hidden="true">์ œ์•ˆํ•˜๊ธฐ</span>
</button>
</div>
</div>
Expand Down
Loading