+++ @fox/core/components/fox-alert/fox-alert.tsx
... | ... | @@ -0,0 +1,132 @@ |
| 1 | +import type { ReactNode, Ref } from "react"; | |
| 2 | +import { | |
| 3 | + FoxChatDotsIcon, | |
| 4 | + FoxCheckCircleIcon, | |
| 5 | + FoxInfoIcon, | |
| 6 | + FoxProhibitIcon, | |
| 7 | + FoxWarningIcon, | |
| 8 | +} from "../../icons"; | |
| 9 | +import { cx } from "../../utils"; | |
| 10 | + | |
| 11 | +export type FoxAlertType = | |
| 12 | + | "default" | |
| 13 | + | "information" | |
| 14 | + | "success" | |
| 15 | + | "warning" | |
| 16 | + | "danger"; | |
| 17 | + | |
| 18 | +export interface FoxAlertProps { | |
| 19 | + /** | |
| 20 | + * 알림의 성격. 아이콘·배경·테두리·제목 색이 여기서 갈린다. | |
| 21 | + * | |
| 22 | + * 계열이 바꾸는 것은 배경 · 테두리 · 제목 글자색과 아이콘이다. 본문 글자색은 계열과 | |
| 23 | + * 무관하게 같다. | |
| 24 | + * | |
| 25 | + * 아이콘은 계열이 정하고 굵기는 시안대로 `duotone`이며, 색은 제목 글자와 같은 계열색이다. | |
| 26 | + */ | |
| 27 | + type?: FoxAlertType; | |
| 28 | + /** 굵은 제목 줄. 넘기지 않으면 영역을 렌더링하지 않는다. */ | |
| 29 | + title?: ReactNode; | |
| 30 | + /** 제목 아래 본문. 넘기지 않으면 영역을 렌더링하지 않는다. */ | |
| 31 | + message?: ReactNode; | |
| 32 | + /** | |
| 33 | + * 보조기술이 읽는 방식을 정한다(`FoxToast`와 같은 규칙). | |
| 34 | + * | |
| 35 | + * - `false`(기본): `role="status"` — 하던 말을 끊지 않는다. 화면과 함께 처음부터 놓여 | |
| 36 | + * 있는 알림은 이쪽이다. | |
| 37 | + * - `true`: `role="alert"` — 읽던 것을 끊고 바로 읽는다. 사용자의 동작 결과로 **그 자리에 | |
| 38 | + * 나타난** 알림에만 쓴다. | |
| 39 | + * | |
| 40 | + * `type="danger"`가 자동으로 이걸 켜지 않는다 — 위험한 내용인 것과 지금 끼어들어야 하는 | |
| 41 | + * 것은 다른 문제이고, 페이지와 함께 그려지는 경고까지 낭독을 끊으면 방해만 된다. | |
| 42 | + */ | |
| 43 | + urgent?: boolean; | |
| 44 | + /** 참이면 렌더하지 않는다(DOM에 남지 않는다). */ | |
| 45 | + hidden?: boolean; | |
| 46 | + id?: string; | |
| 47 | + /** 배치 조정용. 시안의 폭(720px)을 바꿔야 할 때도 여기로 준다. */ | |
| 48 | + className?: string; | |
| 49 | + ref?: Ref<HTMLDivElement>; | |
| 50 | +} | |
| 51 | + | |
| 52 | +/** `Record`로 고정해 계열을 추가하면 항목 누락이 타입 에러가 되게 한다. */ | |
| 53 | +const TYPE_CLASS: Record<FoxAlertType, string> = { | |
| 54 | + default: "fox-alert--default", | |
| 55 | + information: "fox-alert--information", | |
| 56 | + success: "fox-alert--success", | |
| 57 | + warning: "fox-alert--warning", | |
| 58 | + danger: "fox-alert--danger", | |
| 59 | +}; | |
| 60 | + | |
| 61 | +/** | |
| 62 | + * 계열이 정하는 아이콘. 호출부가 넘기지 않는다 — 무엇을 그릴지는 계열이 이미 답한다 | |
| 63 | + * (`FoxTag`와 같은 규칙). | |
| 64 | + * | |
| 65 | + * 시안 `ico/*`가 Phosphor 원본이라 이름이 그대로 붙는다(`ico/Prohibit` → | |
| 66 | + * `FoxProhibitIcon`). 손으로 SVG를 그려 넣지 않는다 — 아이콘은 생성물이 단일 출처다. | |
| 67 | + * | |
| 68 | + * 굵기는 시안이 `Weight=Duotone`이라 다섯 개 모두 `duotone`이다. 기본값(`regular`)으로 두면 | |
| 69 | + * 속이 빈 선 그림이 되어 시안과 달라지므로 반드시 적는다. | |
| 70 | + * | |
| 71 | + * duotone은 연한 면(`opacity` 0.2)과 진한 선을 겹쳐 그리는데 **둘 다 `currentColor`다** — | |
| 72 | + * 색은 스타일이 `.fox-alert__icon`에 준 하나를 쓰고, 연한 쪽만 알아서 흐려진다. | |
| 73 | + */ | |
| 74 | +const TYPE_ICON: Record<FoxAlertType, ReactNode> = { | |
| 75 | + default: <FoxChatDotsIcon weight="duotone" />, | |
| 76 | + information: <FoxInfoIcon weight="duotone" />, | |
| 77 | + success: <FoxCheckCircleIcon weight="duotone" />, | |
| 78 | + warning: <FoxWarningIcon weight="duotone" />, | |
| 79 | + danger: <FoxProhibitIcon weight="duotone" />, | |
| 80 | +}; | |
| 81 | + | |
| 82 | +/** | |
| 83 | + * @fox 알럿. 화면 안에 놓이는 띠 모양 알림이다 — 아이콘 상자 · 제목 · 본문이 한 줄로 선다. | |
| 84 | + * | |
| 85 | + * **모달이 아니다.** 화면을 덮지도, 포커스를 가두지도, 확인 버튼을 갖지도 않는다. 내용 흐름 | |
| 86 | + * 안에 그대로 놓이고, 언제 나타나고 사라질지는 호출부가 정한다(`FoxToast`와 같은 방식). | |
| 87 | + * | |
| 88 | + * 상호작용이 없어 `"use client"`가 아니다 — 서버 컴포넌트로 렌더된다. | |
| 89 | + * | |
| 90 | + * ⚠️ 시안 주석: **본문은 두 줄 이내**로 쓴다. 길이를 코드가 막지는 않는다 — 세 줄이 되면 | |
| 91 | + * 아이콘 상자만 가운데 남고 띠가 늘어난다. | |
| 92 | + * | |
| 93 | + * ⚠️ 스타일은 이 파일이 import하지 않는다 — 호스트 앱이 `@use "@fox/styles/components"` | |
| 94 | + * (또는 개별 파티셜)로 한 번 불러와야 한다. | |
| 95 | + */ | |
| 96 | +export function FoxAlert({ | |
| 97 | + type = "default", | |
| 98 | + title, | |
| 99 | + message, | |
| 100 | + urgent = false, | |
| 101 | + hidden = false, | |
| 102 | + id, | |
| 103 | + className, | |
| 104 | + ref, | |
| 105 | +}: FoxAlertProps) { | |
| 106 | + if (hidden) { | |
| 107 | + return null; | |
| 108 | + } | |
| 109 | + | |
| 110 | + return ( | |
| 111 | + <div | |
| 112 | + ref={ref} | |
| 113 | + id={id} | |
| 114 | + role={urgent ? "alert" : "status"} | |
| 115 | + className={cx("fox-alert", TYPE_CLASS[type], className)} | |
| 116 | + > | |
| 117 | + {/* 아이콘이 정해지기 전에도 상자는 그린다 — 시안이 흰 판을 배치의 일부로 준다. */} | |
| 118 | + <span className="fox-alert__icon" aria-hidden="true"> | |
| 119 | + {TYPE_ICON[type]} | |
| 120 | + </span> | |
| 121 | + | |
| 122 | + <div className="fox-alert__content"> | |
| 123 | + {title === undefined ? null : ( | |
| 124 | + <strong className="fox-alert__title">{title}</strong> | |
| 125 | + )} | |
| 126 | + {message === undefined ? null : ( | |
| 127 | + <span className="fox-alert__message">{message}</span> | |
| 128 | + )} | |
| 129 | + </div> | |
| 130 | + </div> | |
| 131 | + ); | |
| 132 | +} |
+++ @fox/core/components/fox-alert/index.ts
... | ... | @@ -0,0 +1,1 @@ |
| 1 | +export { FoxAlert, type FoxAlertProps, type FoxAlertType } from "./fox-alert"; |
--- @fox/core/components/index.ts
+++ @fox/core/components/index.ts
... | ... | @@ -1,4 +1,5 @@ |
| 1 | 1 |
export * from "./fox-address"; |
| 2 |
+export * from "./fox-alert"; |
|
| 2 | 3 |
export * from "./fox-badge"; |
| 3 | 4 |
export * from "./fox-badge-dot"; |
| 4 | 5 |
export * from "./fox-badge-group"; |
--- @fox/dev-test/component-registry.tsx
+++ @fox/dev-test/component-registry.tsx
... | ... | @@ -3,6 +3,7 @@ |
| 3 | 3 |
import { FoxInput, FoxInputState, FoxSelect, FoxSelectItem, FoxSelectOption, FoxSelectOptionItem } from "@/@fox/core/components";
|
| 4 | 4 |
import { useEffect, useState, type CSSProperties, type ReactNode } from "react";
|
| 5 | 5 |
import { FoxAddress, type FoxAddressSearchResult, type FoxAddressValue } from "../core/components/fox-address";
|
| 6 |
+import { FoxAlert } from "../core/components/fox-alert";
|
|
| 6 | 7 |
import { FoxBreadcrumb } from "../core/components/fox-breadcrumb";
|
| 7 | 8 |
import { FoxButton } from "../core/components/fox-button";
|
| 8 | 9 |
import { FoxButtonGroup } from "../core/components/fox-button-group";
|
... | ... | @@ -3319,4 +3320,53 @@ |
| 3319 | 3320 |
}, |
| 3320 | 3321 |
], |
| 3321 | 3322 |
}, |
| 3323 |
+ {
|
|
| 3324 |
+ id: "fox-alert", |
|
| 3325 |
+ name: "FoxAlert", |
|
| 3326 |
+ description: |
|
| 3327 |
+ "Figma 시안(alert 258:433) 폭 720px은 시안 값 그대로지만 좁은 자리에서 부모를 뚫지 않게 max-inline-size: 100%를 함께 걸었습니다. 계열은 배경·테두리·제목 글자색을 바꿉니다 — 테두리만 -inverse 토큰이라 연한 배경(5) 위에 한 단계 진한 선(10)이 얹힙니다. 본문 글자색과 아이콘 상자의 흰 배경은 계열과 무관합니다.", |
|
| 3328 |
+ variants: [ |
|
| 3329 |
+ {
|
|
| 3330 |
+ label: "default", |
|
| 3331 |
+ node: ( |
|
| 3332 |
+ <FoxAlert |
|
| 3333 |
+ title="알림 제목이 들어갑니다" |
|
| 3334 |
+ message="콘텐츠 내 알럿은 최대 2줄 이내로 작성합니다 콘텐츠 내 알럿은 최대 2줄 이내로 작성합니다 콘텐츠 내 알럿은 최대 2줄 이내로 작성합니다" |
|
| 3335 |
+ /> |
|
| 3336 |
+ ), |
|
| 3337 |
+ }, |
|
| 3338 |
+ {
|
|
| 3339 |
+ label: "제목만 / 본문만 — 넘기지 않은 쪽은 렌더하지 않습니다", |
|
| 3340 |
+ node: ( |
|
| 3341 |
+ <> |
|
| 3342 |
+ <FoxAlert title="제목만 있는 알림" /> |
|
| 3343 |
+ <FoxAlert message="본문만 있는 알림입니다." /> |
|
| 3344 |
+ </> |
|
| 3345 |
+ ), |
|
| 3346 |
+ }, |
|
| 3347 |
+ {
|
|
| 3348 |
+ label: "type 5종", |
|
| 3349 |
+ node: ( |
|
| 3350 |
+ <> |
|
| 3351 |
+ <FoxAlert type="default" title="default" message="기본" /> |
|
| 3352 |
+ <FoxAlert type="information" title="information" message="안내" /> |
|
| 3353 |
+ <FoxAlert type="success" title="success" message="완료" /> |
|
| 3354 |
+ <FoxAlert type="warning" title="warning" message="주의" /> |
|
| 3355 |
+ <FoxAlert type="danger" title="danger" message="위험" /> |
|
| 3356 |
+ </> |
|
| 3357 |
+ ), |
|
| 3358 |
+ }, |
|
| 3359 |
+ {
|
|
| 3360 |
+ label: "좁은 자리(360px)에 넣으면 폭이 부모에 맞춰 줄어듭니다", |
|
| 3361 |
+ node: ( |
|
| 3362 |
+ <div style={{ inlineSize: "36rem", outline: "1px dashed currentColor" }}>
|
|
| 3363 |
+ <FoxAlert |
|
| 3364 |
+ title="좁은 단 안의 알림" |
|
| 3365 |
+ message="720px이 아니라 부모 폭까지만 늘어납니다." |
|
| 3366 |
+ /> |
|
| 3367 |
+ </div> |
|
| 3368 |
+ ), |
|
| 3369 |
+ }, |
|
| 3370 |
+ ], |
|
| 3371 |
+ }, |
|
| 3322 | 3372 |
]; |
+++ @fox/styles/_fox-alert.scss
... | ... | @@ -0,0 +1,166 @@ |
| 1 | +// FoxAlert — 시안: 통합관리자페이지 디자인시스템 Figma alert | |
| 2 | +// | |
| 3 | +// 화면 안에 놓이는 띠 모양 알림이다. 아이콘 상자 · 제목 · 본문이 한 줄로 선다. | |
| 4 | +// **모달이 아니다** — 화면을 덮지 않고 내용 흐름 안에 그대로 놓인다. | |
| 5 | +// | |
| 6 | +// 마크업 계약 (React 밖 소비자용): | |
| 7 | +// <div class="fox-alert fox-alert--default" role="status"> | |
| 8 | +// <span class="fox-alert__icon">…아이콘…</span> | |
| 9 | +// <div class="fox-alert__content"> | |
| 10 | +// <strong class="fox-alert__title">제목</strong> | |
| 11 | +// <span class="fox-alert__message">본문</span> | |
| 12 | +// </div> | |
| 13 | +// </div> | |
| 14 | +// | |
| 15 | +// 제목·본문은 각각 없을 수 있다. 아이콘 상자는 아이콘이 없어도 그린다 — 시안이 흰 판을 | |
| 16 | +// 배치의 일부로 주고, 있고 없고에 따라 글자 시작점이 흔들리면 안 된다. | |
| 17 | +// | |
| 18 | +// 제목이 `heading/xsm`(title 패밀리·bold)이고 본문이 `body/md`(body 패밀리·regular)다. | |
| 19 | +// 글자 크기는 둘 다 15px이라 굵기와 패밀리로만 갈린다 — 시안이 그렇게 준다. | |
| 20 | +// | |
| 21 | +// 토큰이 아닌 값: | |
| 22 | +// - ⚠️ `inline-size: 72rem`: **시안의 폭(720px)을 일단 그대로 적용해 둔 것이다.** Figma가 | |
| 23 | +// 변수로 export하지 않았고 토큰 스케일에도 720이 없다(modal 계열의 가장 큰 값이 760px). | |
| 24 | +// 토큰이 생기면 `fox.*()`로 바꾸고, 폭을 고정하지 않는 쪽으로 정해지면 여기서 뺀다. | |
| 25 | +// 그때까지 폭을 바꿔야 하는 자리는 호출부가 `className`으로 덮는다 | |
| 26 | +// (`fox-toggle-quantity`와 같은 처리). | |
| 27 | +// - `max-inline-size: 100%`: 시안에 없는 구조 규칙이다. 720px보다 좁은 자리에 놓였을 때 | |
| 28 | +// 띠가 부모를 뚫고 나가지 않게 막는다 — 고정 폭만 두면 표 안이나 좁은 단에서 넘친다. | |
| 29 | +// - line-height / letter-spacing: 시안 텍스트 스타일 heading/xsm·body/md의 값이나 Figma가 | |
| 30 | +// 변수로 export하지 않는다. 시안의 -0.375px은 15px 기준이라 `-0.025em`으로 적는다 | |
| 31 | +// (다른 컴포넌트와 같은 근거). | |
| 32 | +// - box-sizing / display / flex / aspect-ratio: 구조 규칙. | |
| 33 | +// | |
| 34 | +// 아이콘은 컴포넌트의 `TYPE_ICON`이 계열마다 하나씩 쥐고 있다(Prohibit · Warning · | |
| 35 | +// CheckCircle · Info · ChatDots, 전부 `weight="duotone"`). 색은 **제목 글자와 같은 계열색**을 | |
| 36 | +// 쓴다(default는 `font-neutral-strong`) — 시안 확인 완료. | |
| 37 | +// | |
| 38 | +// duotone은 연한 면과 진한 선을 겹쳐 그리고 **둘 다 `currentColor`를 쓴다** — 연한 쪽은 | |
| 39 | +// 글리프 자체가 `opacity: 0.2`를 물고 있어 색을 하나만 줘도 두 톤이 나온다. | |
| 40 | +// | |
| 41 | +// duotone은 연한 면과 진한 선을 겹쳐 그리고 **둘 다 `currentColor`를 쓴다** — 연한 쪽은 | |
| 42 | +// 글리프 자체가 `opacity: 0.2`를 물고 있어 색을 하나만 줘도 두 톤이 나온다. | |
| 43 | +// | |
| 44 | + | |
| 45 | +@use "@fox/styles/abstracts" as fox; | |
| 46 | + | |
| 47 | +.fox-alert { | |
| 48 | + // 안쪽 여백이 바깥 폭을 늘리지 않게 한다 — 720px이 720px로 그려진다. | |
| 49 | + box-sizing: border-box; | |
| 50 | + display: flex; | |
| 51 | + align-items: center; | |
| 52 | + gap: fox.gap(4); | |
| 53 | + inline-size: 72rem; | |
| 54 | + max-inline-size: 100%; | |
| 55 | + padding: fox.padding(5) fox.padding(6); | |
| 56 | + border: fox.border(1) solid fox.color(border-neutral-subtler); | |
| 57 | + border-radius: fox.radius(4); | |
| 58 | + background: fox.color(surface-neutral-gray); | |
| 59 | + | |
| 60 | + // 정사각 흰 판. 글자가 길어져 띠가 늘어나도 크기가 그대로여야 해서 줄어들지 않게 막는다. | |
| 61 | + &__icon { | |
| 62 | + display: flex; | |
| 63 | + flex-shrink: 0; | |
| 64 | + justify-content: center; | |
| 65 | + align-items: center; | |
| 66 | + inline-size: fox.number(13); | |
| 67 | + block-size: fox.number(13); | |
| 68 | + aspect-ratio: 1 / 1; | |
| 69 | + border-radius: fox.radius(4); | |
| 70 | + background: fox.color(element-neutral-inverse); | |
| 71 | + // 글리프가 `fill: currentColor`라 이 색을 그대로 입는다. 계열이 있으면 아래에서 덮는다. | |
| 72 | + color: fox.color(font-neutral-strong); | |
| 73 | + } | |
| 74 | + | |
| 75 | + // 글리프는 흰 판(36px)보다 작은 24px이다 — 판이 글리프를 감싸는 여백을 갖는다. | |
| 76 | + // 상자와 마찬가지로 줄어들지 않게 막고 정사각을 못박는다(시안이 둘 다 명시한다). | |
| 77 | + &__icon > * { | |
| 78 | + flex-shrink: 0; | |
| 79 | + inline-size: fox.icon(5); | |
| 80 | + block-size: fox.icon(5); | |
| 81 | + aspect-ratio: 1 / 1; | |
| 82 | + } | |
| 83 | + | |
| 84 | + // 남는 폭을 전부 가져간다. `min-inline-size: 0`이 없으면 긴 글이 줄바꿈되지 않고 띠를 | |
| 85 | + // 밀어 넓힌다(flex 자식의 기본 최소 크기가 내용 크기라서다). | |
| 86 | + &__content { | |
| 87 | + display: flex; | |
| 88 | + flex: 1 0 0; | |
| 89 | + flex-direction: column; | |
| 90 | + align-items: flex-start; | |
| 91 | + gap: fox.gap(1); | |
| 92 | + min-inline-size: 0; | |
| 93 | + } | |
| 94 | + | |
| 95 | + &__title { | |
| 96 | + align-self: stretch; | |
| 97 | + color: fox.color(font-neutral-strong); | |
| 98 | + font-family: fox.font-family(title); | |
| 99 | + font-size: fox.font-size(heading-xsm); | |
| 100 | + font-weight: fox.font-weight(bold); | |
| 101 | + line-height: 1.4; | |
| 102 | + letter-spacing: -0.025em; | |
| 103 | + } | |
| 104 | + | |
| 105 | + &__message { | |
| 106 | + align-self: stretch; | |
| 107 | + color: fox.color(font-neutral-strong); | |
| 108 | + font-family: fox.font-family(body); | |
| 109 | + font-size: fox.font-size(body-md); | |
| 110 | + font-weight: fox.font-weight(regular); | |
| 111 | + line-height: 1.5; | |
| 112 | + letter-spacing: -0.025em; | |
| 113 | + } | |
| 114 | + | |
| 115 | + // ── 계열 ──────────────────────────────────────────────────────────────────── | |
| 116 | + // 계열이 바꾸는 것은 **배경 · 테두리 · 제목 글자색** 셋뿐이다. 본문 글자색과 아이콘 상자의 | |
| 117 | + // 흰 배경은 계열과 무관하게 블록 규칙을 그대로 쓴다(시안 확인). | |
| 118 | + // | |
| 119 | + // 테두리만 `-inverse`다 — 같은 계열의 5(연한 배경)와 10(그보다 한 단계 진한 선)을 짝지어 | |
| 120 | + // 쓰라고 토큰이 그렇게 갈라져 있다. 다크 모드에서는 세 값이 함께 뒤집힌다(95 / 90 / 20). | |
| 121 | + // | |
| 122 | + // `default`는 블록 규칙이 곧 그 계열의 모양이라 따로 덮을 것이 없다. | |
| 123 | + &--default { | |
| 124 | + // 시안 그대로 — 블록에 있는 값이 전부다. | |
| 125 | + } | |
| 126 | + | |
| 127 | + &--information { | |
| 128 | + border-color: fox.color(border-system-information-inverse); | |
| 129 | + background: fox.color(surface-system-information); | |
| 130 | + } | |
| 131 | + | |
| 132 | + &--information &__title, | |
| 133 | + &--information &__icon { | |
| 134 | + color: fox.color(font-system-information); | |
| 135 | + } | |
| 136 | + | |
| 137 | + &--success { | |
| 138 | + border-color: fox.color(border-system-success-inverse); | |
| 139 | + background: fox.color(surface-system-success); | |
| 140 | + } | |
| 141 | + | |
| 142 | + &--success &__title, | |
| 143 | + &--success &__icon { | |
| 144 | + color: fox.color(font-system-success); | |
| 145 | + } | |
| 146 | + | |
| 147 | + &--warning { | |
| 148 | + border-color: fox.color(border-system-warning-inverse); | |
| 149 | + background: fox.color(surface-system-warning); | |
| 150 | + } | |
| 151 | + | |
| 152 | + &--warning &__title, | |
| 153 | + &--warning &__icon { | |
| 154 | + color: fox.color(font-system-warning); | |
| 155 | + } | |
| 156 | + | |
| 157 | + &--danger { | |
| 158 | + border-color: fox.color(border-system-danger-inverse); | |
| 159 | + background: fox.color(surface-system-danger); | |
| 160 | + } | |
| 161 | + | |
| 162 | + &--danger &__title, | |
| 163 | + &--danger &__icon { | |
| 164 | + color: fox.color(font-system-danger); | |
| 165 | + } | |
| 166 | +} |
--- @fox/styles/components.scss
+++ @fox/styles/components.scss
... | ... | @@ -8,6 +8,7 @@ |
| 8 | 8 |
@use "fox-button-group"; |
| 9 | 9 |
@use "fox-button-panel"; |
| 10 | 10 |
@use "fox-address"; |
| 11 |
+@use "fox-alert"; |
|
| 11 | 12 |
@use "fox-certified"; |
| 12 | 13 |
@use "fox-check-icon"; |
| 13 | 14 |
@use "fox-checkbox"; |
... | ... | @@ -51,4 +52,4 @@ |
| 51 | 52 |
@use "fox-tab-item"; |
| 52 | 53 |
@use "fox-spinner"; |
| 53 | 54 |
@use "fox-toast"; |
| 54 |
-@use "fox-toggle-quantity"; |
|
| 55 |
+@use "fox-toggle-quantity";(파일 끝에 줄바꿈 문자 없음) |
Add a comment
Delete comment
Once you delete this comment, you won't be able to recover it. Are you sure you want to delete this comment?