diff --git a/src/components/content/YoutubePreviewPlayer.test.tsx b/src/components/content/YoutubePreviewPlayer.test.tsx index 94c50208..b29b4eb4 100644 --- a/src/components/content/YoutubePreviewPlayer.test.tsx +++ b/src/components/content/YoutubePreviewPlayer.test.tsx @@ -71,4 +71,21 @@ describe("YoutubePreviewPlayer", () => { act(() => window.dispatchEvent(new Event("scroll"))); expect(pause).toHaveBeenCalled(); }); + + it("cropEdges가 설정되면 영상의 가장자리 픽셀을 확대해 잘라낸다", () => { + const { container } = render( + , + ); + + expect(screen.getByLabelText("Cropped AIP video")).toHaveClass("scale-[1.01]"); + expect(container.firstElementChild).toHaveClass("feature-gif-frame"); + }); }); diff --git a/src/components/content/YoutubePreviewPlayer.tsx b/src/components/content/YoutubePreviewPlayer.tsx index 6e1564e9..26d4926f 100644 --- a/src/components/content/YoutubePreviewPlayer.tsx +++ b/src/components/content/YoutubePreviewPlayer.tsx @@ -5,7 +5,9 @@ import MediaPlayButton from "@/components/ui/MediaPlayButton"; type YoutubePreviewPlayerProps = { autoplayOnView?: boolean; + cropEdges?: boolean; embedSrc?: string; + framed?: boolean; thumbnailAlt: string; thumbnailSrc: string; title: string; @@ -25,7 +27,9 @@ function withPlaybackParams(src: string) { export default function YoutubePreviewPlayer({ autoplayOnView = false, + cropEdges = false, embedSrc, + framed = false, thumbnailAlt, thumbnailSrc, title, @@ -33,6 +37,7 @@ export default function YoutubePreviewPlayer({ }: YoutubePreviewPlayerProps) { const [isPlaying, setIsPlaying] = useState(false); const videoRef = useRef(null); + const videoClassName = `block h-full w-full bg-black object-cover ${cropEdges ? "scale-[1.01]" : ""}`; useEffect(() => { if (!autoplayOnView || !videoSrc) return; @@ -81,11 +86,11 @@ export default function YoutubePreviewPlayer({ }, [autoplayOnView, videoSrc]); return ( -
+
{autoplayOnView && videoSrc ? (
- +
{/* 법적 링크 */} diff --git a/src/components/layout/admin/AdminShell.tsx b/src/components/layout/admin/AdminShell.tsx index 631ec95f..c34b2f39 100644 --- a/src/components/layout/admin/AdminShell.tsx +++ b/src/components/layout/admin/AdminShell.tsx @@ -179,7 +179,7 @@ export default function AdminShell({ children }: AdminShellProps) { ))}
- +
diff --git a/src/components/layout/admin/AdminSidebar.tsx b/src/components/layout/admin/AdminSidebar.tsx index 2b1ccac4..ecda0176 100644 --- a/src/components/layout/admin/AdminSidebar.tsx +++ b/src/components/layout/admin/AdminSidebar.tsx @@ -174,7 +174,7 @@ export default function AdminSidebar({
diff --git a/src/components/pages/community-license/apply/CommunityLicenseApplyPage.tsx b/src/components/pages/community-license/apply/CommunityLicenseApplyPage.tsx index be3421dd..4ab888f4 100644 --- a/src/components/pages/community-license/apply/CommunityLicenseApplyPage.tsx +++ b/src/components/pages/community-license/apply/CommunityLicenseApplyPage.tsx @@ -38,7 +38,7 @@ export default function CommunityLicenseApplyPage(copy: CommunityLicenseApplyPag

{item.label}:{" "} {item.value} @@ -51,7 +51,7 @@ export default function CommunityLicenseApplyPage(copy: CommunityLicenseApplyPag

{supportLink.title}

{item.label}:{" "} {item.value} @@ -55,7 +55,7 @@ export default function ContactUsPage(props: ContactUsPageProps) {

{supportLink.title}

-
+

{title}

{description}

- + {action}
{copy.sentenceBeforePrivacy}{" "} {copy.privacyPolicyLinkLabel} @@ -100,7 +100,7 @@ export default function CookieConsentBanner({ locale }: CookieConsentBannerProps
{copy.sentenceBeforeCookiePreference}{" "}
{copy.cookiePreferenceLinkLabel} diff --git a/src/components/site/ThemeSwitch.test.tsx b/src/components/site/ThemeSwitch.test.tsx index b8bf182f..32b2d7fc 100644 --- a/src/components/site/ThemeSwitch.test.tsx +++ b/src/components/site/ThemeSwitch.test.tsx @@ -30,24 +30,26 @@ describe("ThemeSwitch", () => { it("시스템, 라이트, 다크 옵션을 표시하고 선택한 테마를 저장한다", async () => { render(); - const themeSelect = screen.getByRole("combobox", { name: "컬러 테마 선택" }); - expect(themeSelect).toHaveValue("system"); - expect(screen.getByRole("option", { name: "시스템 모드" })).toBeInTheDocument(); - expect(screen.getByRole("option", { name: "라이트 모드" })).toBeInTheDocument(); - expect(screen.getByRole("option", { name: "다크 모드" })).toBeInTheDocument(); + const systemTheme = screen.getByRole("radio", { name: "시스템 모드" }); + const lightTheme = screen.getByRole("radio", { name: "라이트 모드" }); + const darkTheme = screen.getByRole("radio", { name: "다크 모드" }); + expect(screen.getByRole("radiogroup", { name: "컬러 테마 선택" })).toBeInTheDocument(); + expect(systemTheme).toBeChecked(); + expect(lightTheme).not.toBeChecked(); + expect(darkTheme).not.toBeChecked(); - fireEvent.change(themeSelect, { target: { value: "dark" } }); + fireEvent.click(darkTheme); await waitFor(() => { - expect(themeSelect).toHaveValue("dark"); + expect(darkTheme).toBeChecked(); }); expect(document.documentElement.dataset.theme).toBe("dark"); expect(localStorage.getItem(THEME_STORAGE_KEY)).toBe("dark"); - fireEvent.change(themeSelect, { target: { value: "system" } }); + fireEvent.click(systemTheme); await waitFor(() => { - expect(themeSelect).toHaveValue("system"); + expect(systemTheme).toBeChecked(); }); expect(document.documentElement.dataset.theme).toBe("light"); expect(localStorage.getItem(THEME_STORAGE_KEY)).toBeNull(); @@ -60,7 +62,7 @@ describe("ThemeSwitch", () => { render(); await waitFor(() => { - expect(screen.getByRole("combobox", { name: "Select color theme" })).toHaveValue("system"); + expect(screen.getByRole("radio", { name: "System mode" })).toBeChecked(); }); expect(document.documentElement.dataset.theme).toBe("dark"); }); @@ -71,7 +73,7 @@ describe("ThemeSwitch", () => { render(); await waitFor(() => { - expect(screen.getByRole("combobox", { name: "Select color theme" })).toHaveValue("system"); + expect(screen.getByRole("radio", { name: "System mode" })).toBeChecked(); }); const changeHandler = addEventListener.mock.calls.find(([eventName]) => eventName === "change")?.[1]; diff --git a/src/components/site/ThemeSwitch.tsx b/src/components/site/ThemeSwitch.tsx index 39a6eff9..e4fa39bb 100644 --- a/src/components/site/ThemeSwitch.tsx +++ b/src/components/site/ThemeSwitch.tsx @@ -1,9 +1,8 @@ "use client"; -import { useEffect, useState } from "react"; +import { useEffect, useId, useState } from "react"; import { isTheme, - isThemePreference, resolveTheme, THEME_CHANGE_EVENT, THEME_STORAGE_KEY, @@ -97,6 +96,7 @@ export default function ThemeSwitch({ locale = "en", }: ThemeSwitchProps) { const [preference, setPreference] = useState("system"); + const groupName = useId(); useEffect(() => { const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); @@ -134,62 +134,63 @@ export default function ThemeSwitch({ }, []); const copy = themeCopy[locale]; + const preferences: ThemePreference[] = ["system", "light", "dark"]; + + const selectTheme = (nextPreference: ThemePreference) => { + if (nextPreference === "system") { + localStorage.removeItem(THEME_STORAGE_KEY); + } else { + localStorage.setItem(THEME_STORAGE_KEY, nextPreference); + } + + const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches; + applyTheme(resolveTheme(nextPreference, prefersDark)); + setPreference(nextPreference); + window.dispatchEvent(new Event(THEME_CHANGE_EVENT)); + }; return (
- {compact ? null : ( - - {copy[preference]} - - )} - - + {preferences.map((option) => ( + + ))} +
); } diff --git a/src/components/ui/Button.test.tsx b/src/components/ui/Button.test.tsx index 80a12abf..8d3e3068 100644 --- a/src/components/ui/Button.test.tsx +++ b/src/components/ui/Button.test.tsx @@ -42,12 +42,12 @@ describe("Button", () => { render(); expect(screen.getByRole("button").className).toContain("border"); expect(screen.getByRole("button").className).toContain("border-border"); - expect(screen.getByRole("button").className).toContain("hover:bg-secondary"); + expect(screen.getByRole("button").className).toContain("hover:bg-outline-hover"); }); - it("outline hover 상태에는 secondary 배경을 적용한다", () => { + it("outline hover 상태에는 옅은 outline hover 배경을 적용한다", () => { render(); - expect(screen.getByRole("button").className).toContain("bg-secondary"); + expect(screen.getByRole("button").className).toContain("bg-outline-hover"); }); it("type 기본값은 button이다", () => { diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx index 79f5c65b..25c42516 100644 --- a/src/components/ui/Button.tsx +++ b/src/components/ui/Button.tsx @@ -56,8 +56,8 @@ export function getButtonStyle( variant === "outline" && cx( "border border-border", - state === "hover" ? "bg-secondary" : "bg-transparent", - "hover:bg-secondary", + state === "hover" ? "bg-outline-hover" : "bg-transparent", + "hover:bg-outline-hover", ), variant === "primary" && cx(state === "hover" ? "bg-primary-hover" : "bg-primary", "hover:bg-primary-hover"), diff --git a/src/components/ui/TextButton.test.tsx b/src/components/ui/TextButton.test.tsx index b06368ee..360394b8 100644 --- a/src/components/ui/TextButton.test.tsx +++ b/src/components/ui/TextButton.test.tsx @@ -5,7 +5,7 @@ import TextButton from "./TextButton"; describe("TextButton", () => { it("기본으로 more 텍스트 버튼 스타일과 화살표 아이콘을 렌더링한다", () => { const { container } = render(More); - expect(screen.getByRole("button", { name: /More/ }).className).toContain("text-brand"); + expect(screen.getByRole("button", { name: /More/ }).className).toContain("text-link"); expect(container.querySelector("svg")).toBeInTheDocument(); }); diff --git a/src/components/ui/TextButton.tsx b/src/components/ui/TextButton.tsx index f1cb3cac..3aff2ddc 100644 --- a/src/components/ui/TextButton.tsx +++ b/src/components/ui/TextButton.tsx @@ -55,9 +55,9 @@ export default function TextButton({ }: TextButtonProps) { const resolvedState = disabled ? "disable" : state; const classes = cx( - "pressable group inline-flex items-center justify-center gap-1.5 bg-transparent p-0 text-brand hover:text-fg", + "pressable group inline-flex items-center justify-center gap-1.5 bg-transparent p-0 text-link hover:text-link-hover", !className?.includes("type-") && "type-body-md", - resolvedState === "hover" && "text-fg", + resolvedState === "hover" && "text-link-hover", resolvedState === "disable" && "cursor-not-allowed opacity-40", !disabled && "cursor-pointer", className, diff --git a/src/constants/layout.ts b/src/constants/layout.ts index f2a8fe3c..e084a5e0 100644 --- a/src/constants/layout.ts +++ b/src/constants/layout.ts @@ -1,4 +1,4 @@ export const pageSectionGapClassName = "gap-[100px] md:gap-[120px]"; -export const featureItemGapClassName = "gap-[60px] md:gap-[100px]"; +export const featureItemGapClassName = "gap-[60px] md:gap-[80px]"; export const pageTopPaddingClassName = "pt-[calc(100px+var(--language-banner-offset,0px))] md:pt-[calc(140px+var(--language-banner-offset,0px))]"; export const pageXPaddingClassName = "px-5 md:px-10"; diff --git a/src/features/content/previewStyles.ts b/src/features/content/previewStyles.ts index 0bb3abb4..466c84e3 100644 --- a/src/features/content/previewStyles.ts +++ b/src/features/content/previewStyles.ts @@ -16,4 +16,4 @@ export const CONTENT_PREVIEW_TABLE_HEADER_CELL_CLASS = "border-b border-r border export const CONTENT_PREVIEW_TABLE_CELL_CLASS = "border-b border-r border-border px-3 py-3 align-top type-content-table break-words text-[var(--color-content-table-cell-fg)] last:border-r-0"; export const CONTENT_PREVIEW_IMAGE_CAPTION_CLASS = "m-0 text-center type-content-caption text-[var(--color-content-image-caption-fg)]"; export const CONTENT_PREVIEW_RICH_CLASS = - "rich-content flex flex-col gap-4 text-fg [&_a]:text-brand [&_a]:transition-colors [&_a:hover]:text-fg [&_blockquote]:m-0 [&_blockquote]:border-l-2 [&_blockquote]:border-border [&_blockquote]:pl-4 [&_figure]:mx-auto [&_figure]:my-0 [&_figure]:flex [&_figure]:max-w-full [&_figure]:flex-col [&_figure]:gap-3 [&_figcaption]:m-0 [&_figcaption]:text-center [&_figcaption]:type-content-caption [&_figcaption]:text-[var(--color-content-image-caption-fg)] [&_hr]:border-0 [&_hr]:border-t [&_hr]:border-border [&_strong]:font-bold [&_h1]:m-0 [&_h1]:type-content-h1 [&_h1]:pt-10 [&_h1_strong]:font-inherit [&_h2]:m-0 [&_h2]:pt-8 [&_h2]:type-content-h2 [&_h2_strong]:font-inherit [&_h3]:m-0 [&_h3]:pt-6 [&_h3]:type-content-h3 [&_h3_strong]:font-inherit [&_iframe]:aspect-video [&_iframe]:w-full [&_iframe]:rounded-box [&_iframe]:border-0 [&_img]:mx-auto [&_img]:block [&_img]:h-auto [&_img]:max-w-full [&_img]:rounded-box [&_img]:bg-bg-content [&_ol]:m-0 [&_ol]:list-decimal [&_ol]:pl-6 [&_p]:m-0 [&_p]:type-content-body [&_table]:w-full [&_table]:border-separate [&_table]:border-spacing-0 [&_table]:border [&_table]:border-border [&_table]:text-left [&_tbody_tr]:bg-bg [&_td]:border-b [&_td]:border-r [&_td]:border-border [&_td]:px-3 [&_td]:py-3 [&_td]:align-top [&_td]:type-content-table [&_td]:break-words [&_td]:text-[var(--color-content-table-cell-fg)] [&_td_p]:type-content-table [&_td_p]:break-words [&_td:last-child]:border-r-0 [&_tbody_tr:last-child_>_td]:border-b-0 [&_th]:border-b [&_th]:border-r [&_th]:border-border [&_th]:bg-bg-deep [&_th]:px-3 [&_th]:py-3 [&_th]:text-left [&_th]:type-content-table [&_th]:break-words [&_th]:text-[var(--color-content-table-header-fg)] [&_th_p]:type-content-table [&_th_p]:break-words [&_th:last-child]:border-r-0 [&_ul]:m-0 [&_ul]:list-disc [&_ul]:pl-6 [&_video]:block [&_video]:h-full [&_video]:w-full [&_video]:rounded-box [&_video]:bg-bg-content"; + "rich-content flex flex-col gap-4 text-fg [&_a]:text-link [&_a]:transition-colors [&_a:hover]:text-link-hover [&_blockquote]:m-0 [&_blockquote]:border-l-2 [&_blockquote]:border-border [&_blockquote]:pl-4 [&_figure]:mx-auto [&_figure]:my-0 [&_figure]:flex [&_figure]:max-w-full [&_figure]:flex-col [&_figure]:gap-3 [&_figcaption]:m-0 [&_figcaption]:text-center [&_figcaption]:type-content-caption [&_figcaption]:text-[var(--color-content-image-caption-fg)] [&_hr]:border-0 [&_hr]:border-t [&_hr]:border-border [&_strong]:font-bold [&_h1]:m-0 [&_h1]:type-content-h1 [&_h1]:pt-10 [&_h1_strong]:font-inherit [&_h2]:m-0 [&_h2]:pt-8 [&_h2]:type-content-h2 [&_h2_strong]:font-inherit [&_h3]:m-0 [&_h3]:pt-6 [&_h3]:type-content-h3 [&_h3_strong]:font-inherit [&_iframe]:aspect-video [&_iframe]:w-full [&_iframe]:rounded-box [&_iframe]:border-0 [&_img]:mx-auto [&_img]:block [&_img]:h-auto [&_img]:max-w-full [&_img]:rounded-box [&_img]:bg-bg-content [&_ol]:m-0 [&_ol]:list-decimal [&_ol]:pl-6 [&_p]:m-0 [&_p]:type-content-body [&_table]:w-full [&_table]:border-separate [&_table]:border-spacing-0 [&_table]:border [&_table]:border-border [&_table]:text-left [&_tbody_tr]:bg-bg [&_td]:border-b [&_td]:border-r [&_td]:border-border [&_td]:px-3 [&_td]:py-3 [&_td]:align-top [&_td]:type-content-table [&_td]:break-words [&_td]:text-[var(--color-content-table-cell-fg)] [&_td_p]:type-content-table [&_td_p]:break-words [&_td:last-child]:border-r-0 [&_tbody_tr:last-child_>_td]:border-b-0 [&_th]:border-b [&_th]:border-r [&_th]:border-border [&_th]:bg-bg-deep [&_th]:px-3 [&_th]:py-3 [&_th]:text-left [&_th]:type-content-table [&_th]:break-words [&_th]:text-[var(--color-content-table-header-fg)] [&_th_p]:type-content-table [&_th_p]:break-words [&_th:last-child]:border-r-0 [&_ul]:m-0 [&_ul]:list-disc [&_ul]:pl-6 [&_video]:block [&_video]:h-full [&_video]:w-full [&_video]:rounded-box [&_video]:bg-bg-content"; diff --git a/src/styles/globals.css b/src/styles/globals.css index 37e47861..6ac96f69 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -34,8 +34,8 @@ :root, :root[data-theme="light"] { color-scheme: light; - --color-fg: #2F2D29; - --color-mute: #888888; + --color-fg: #26251E; + --color-mute: #787471; --color-placeholder: #aaaaaa; --color-bg-content: #f5f5f4; --color-bg-deep: #f5f5f4; @@ -48,12 +48,13 @@ --color-shadow-rgb: 15 15 15; --color-border: #e3e3e3; --color-border-strong: #B8B8B8; - --color-primary: #2F2D29; + --color-primary: #26251E; --color-primary-hover: #44423e; --color-secondary: #f1f0f0; --color-secondary-hover: #ebeaea; - --color-bg-gnb-popover-rgb: 242 242 242; - --color-surface-popover-rgb: 242 242 242; + --color-outline-hover: #F8F7F5; + --color-bg-gnb-popover-rgb: 242 241 238; + --color-surface-popover-rgb: 242 241 238; --opacity-popover-surface: 0.9; --color-white: #FFFFFF; --color-point: #567000; @@ -64,8 +65,8 @@ --color-chart-secondary: #20A957; --color-brand: #dc4e1a; --color-on-brand: #FFFFFF; - --color-link: #2859B8; - --color-link-hover: #18438F; + --color-link: var(--color-brand); + --color-link-hover: #F07848; --color-info-surface: #EEF5FF; --color-inverse-bg: #2F2D29; --color-inverse-fg: #F7F7F7; @@ -113,6 +114,7 @@ --color-primary-hover: #ABABAB; --color-secondary: #2E2E2E; --color-secondary-hover: #3a3a3a; + --color-outline-hover: #1F1F1F; --color-bg-gnb-popover-rgb: 40 40 40; --color-surface-popover-rgb: 40 40 40; --opacity-popover-surface: 0.9; @@ -125,8 +127,8 @@ --color-chart-secondary: #22C55E; --color-brand: #EF7442; --color-on-brand: #0F0F0F; - --color-link: #7EA2FF; - --color-link-hover: #A9C0FF; + --color-link: var(--color-brand); + --color-link-hover: #FFA078; --color-info-surface: #172238; --color-inverse-bg: #EEEEEE; --color-inverse-fg: #0F0F0F; @@ -260,6 +262,12 @@ body { box-shadow: 0 0 0 4px var(--color-feature-media-frame); } +@media (min-width: 1024px) { + .home-feature-media-list .feature-video-frame { + width: min(681.333px, 65%); + } +} + /* 불투명한 흰 배경을 가진 홈 제품 아이콘을 라이트 테마 표면색과 합성한다. */ .home-feature-icon-surface img { mix-blend-mode: multiply; diff --git a/tailwind.config.ts b/tailwind.config.ts index 0fc144b0..494cd942 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -58,6 +58,7 @@ const config: Config = { "primary-hover": "var(--color-primary-hover)", secondary: "var(--color-secondary)", "secondary-hover": "var(--color-secondary-hover)", + "outline-hover": "var(--color-outline-hover)", white: "var(--color-white)", point: "var(--color-point)", success: "var(--color-success)",