diff --git a/src/design-system/tokens.ts b/src/design-system/tokens.ts index 222133ef..3572a49e 100644 --- a/src/design-system/tokens.ts +++ b/src/design-system/tokens.ts @@ -2,7 +2,6 @@ -import { px } from 'framer-motion'; @@ -460,6 +459,79 @@ export const fontSize = { '6xl': '3.75rem', } as const; +// ─── 역할 기반 타이포그래피 (앞으로 이것만 쓸 것) ────────────────────────── +export const typographyRole = { + display: { + fontSize: 'clamp(32px, 5vw, 56px)', + fontWeight: 700, + letterSpacing: '-0.03em', + lineHeight: 1.2, + }, + sectionTitle: { + fontSize: 'clamp(22px, 2.6vw, 30px)', + fontWeight: 700, + letterSpacing: '-0.02em', + lineHeight: 1.3, + }, + cardTitle: { + fontSize: 'clamp(16px, 1.4vw, 18px)', + fontWeight: 600, + letterSpacing: '-0.01em', + lineHeight: 1.4, + }, + body: { + fontSize: '15px', + fontWeight: 400, + lineHeight: 1.7, + }, + bodySm: { + fontSize: '14px', + fontWeight: 400, + lineHeight: 1.6, + }, + caption: { + fontSize: '13px', + fontWeight: 400, + lineHeight: 1.5, + }, + label: { + fontSize: '12px', + fontWeight: 500, + letterSpacing: '0.02em', + lineHeight: 1.4, + }, + eyebrow: { + fontSize: '12px', + fontWeight: 500, + letterSpacing: '0.12em', + lineHeight: 1.4, + }, + numeric: { + fontSize: 'clamp(28px, 4vw, 44px)', + fontWeight: 700, + letterSpacing: '-0.03em', + fontVariantNumeric: 'tabular-nums' as const, + }, +} as const; + +// ─── 레이아웃 간격 토큰 ──────────────────────────────────────────────────── +export const spacingLayout = { + pageX: 'clamp(20px, 5vw, 40px)', + pageTop: 'clamp(48px, 8vh, 80px)', + sectionGap: 'clamp(56px, 9vh, 96px)', + cardPadding:'clamp(16px, 2vw, 20px)', + cardGap: 'clamp(12px, 1.5vw, 16px)', +} as const; + +// ─── 미디어 쿼리 헬퍼 ───────────────────────────────────────────────────── +export const mq = { + mobile: '@media (max-width: 767px)', + tablet: '@media (min-width: 768px) and (max-width: 1023px)', + laptop: '@media (min-width: 1024px)', + desktop: '@media (min-width: 1280px)', + tabletSm: '@media (min-width: 640px)', // sorimaru/core-ui 640px 대응 +} as const; + export const fluidHeading = { @@ -533,8 +605,7 @@ export const createTheme = (mode: ColorMode) => { p1: '18px', p2: '16px', p3: '14px', p4: '12px', headline: '18px', headlineCaps: '14px', inputField: '16px', }, - - + role: typographyRole, fontWeight: { thin: 100, light: 300, regular: 400, medium: 500, bold: 700 }, lineHeight: { tight: 1.25, normal: 1.6, loose: 1.8 }, }, @@ -543,6 +614,7 @@ export const createTheme = (mode: ColorMode) => { 0: '0', 1: '0.25rem', 2: '0.5rem', 3: '0.75rem', 4: '1rem', 5: '1.25rem', 6: '1.5rem', 8: '2rem', 10: '2.5rem', 12: '3rem', 16: '4rem', 20: '5rem', 24: '6rem', + layout: spacingLayout, }, borderRadius: { diff --git a/src/features/hanok-archive/HanokArchive.tsx b/src/features/hanok-archive/HanokArchive.tsx index 98831eac..55fae889 100644 --- a/src/features/hanok-archive/HanokArchive.tsx +++ b/src/features/hanok-archive/HanokArchive.tsx @@ -4,7 +4,7 @@ import React, { useEffect, useState } from 'react'; import dynamic from 'next/dynamic'; import styled from '@emotion/styled'; import { Global, css } from '@emotion/react'; -import { meok, lightPalette, surface, fontSize } from '@/design-system/tokens'; +import { meok, lightPalette, surface, typographyRole, spacingLayout, mq } from '@/design-system/tokens'; import HanokGrid from '@/features/hanok-archive/sections/HanokGrid'; import HanokStayAccordion from '@/features/hanok-archive/sections/HanokStayAccordion'; import HanokMap from '@/features/hanok-archive/sections/HanokMap'; @@ -60,9 +60,9 @@ const PageInner = styled.div` width: 100%; margin: 0; padding-top: 76px; - padding-bottom: clamp(64px, 8vh, 120px); + padding-bottom: ${spacingLayout.sectionGap}; - @media (max-width: 767px) { + ${mq.mobile} { padding-top: 20px; } `; @@ -71,16 +71,15 @@ const SectionContainer = styled.div` margin: 0 auto; width: min(calc(100% - 40px), 1140px); max-width: 1140px; - padding: clamp(28px, 4vw, 48px) 0; + padding: ${spacingLayout.sectionGap} 0; - @media (max-width: 1024px) { + @media (max-width: 1023px) { width: calc(100% - 28px); - padding: clamp(24px, 4vw, 40px) 0; + padding: ${spacingLayout.sectionGap} 0; } - @media (max-width: 640px) { + ${mq.mobile} { width: calc(100% - 24px); - padding: clamp(20px, 4vw, 32px) 0; } `; @@ -106,12 +105,12 @@ const paperGround = css` const ChapterBreak = styled.div` - padding-top: clamp(80px, 9vh, 124px); + padding-top: ${spacingLayout.sectionGap}; `; const ArchiveSection = styled.div` - padding-top: clamp(64px, 7vh, 92px); + padding-top: ${spacingLayout.sectionGap}; `; const IntroStage = styled.div` @@ -126,12 +125,12 @@ const IntroStage = styled.div` align-items: center; justify-content: center; - @media (max-width: 1024px) { + @media (max-width: 1023px) { width: calc(100% - 28px); margin: 0 auto; } - @media (max-width: 640px) { + ${mq.mobile} { width: calc(100% - 24px); margin: 0 auto; border-radius: 18px; @@ -183,7 +182,7 @@ const Lead = styled.p` `; const SourceNote = styled.p` - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; color: ${meok[500]}; margin: 0; diff --git a/src/features/hanok-archive/components/ArchiveNav.tsx b/src/features/hanok-archive/components/ArchiveNav.tsx index 77e26afa..304b05d7 100644 --- a/src/features/hanok-archive/components/ArchiveNav.tsx +++ b/src/features/hanok-archive/components/ArchiveNav.tsx @@ -4,7 +4,7 @@ import React, { useState } from 'react'; import styled from '@emotion/styled'; import Link from 'next/link'; import type { SectionId } from '@/features/hanok-archive/types'; -import { lightPalette, meok , fontSize } from '@/design-system/tokens'; +import { lightPalette, meok, typographyRole } from '@/design-system/tokens'; const Nav = styled.nav` position: sticky; @@ -23,7 +23,7 @@ const Nav = styled.nav` `; const LogoLink = styled(Link)` - font-size: ${fontSize.base}; + font-size: ${typographyRole.body.fontSize}; font-weight: 500; color: ${meok[900]}; text-decoration: none; @@ -50,7 +50,7 @@ const MenuList = styled.ul` const MenuItem = styled.li``; const MenuAnchor = styled.a<{ $active: boolean }>` - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; font-weight: ${({ $active }) => ($active ? 500 : 400)}; color: ${({ $active }) => ($active ? meok[900] : meok[700])}; text-decoration: none; diff --git a/src/features/hanok-archive/components/FilterBar.tsx b/src/features/hanok-archive/components/FilterBar.tsx index 88c4b5da..eeab0801 100644 --- a/src/features/hanok-archive/components/FilterBar.tsx +++ b/src/features/hanok-archive/components/FilterBar.tsx @@ -5,7 +5,7 @@ import styled from '@emotion/styled'; import { motion } from 'framer-motion'; import { HugeiconsIcon } from '@hugeicons/react' import { Cancel01Icon, ChevronDownIcon, RotateCcwIcon, Search01Icon, Tag01Icon } from '@hugeicons/core-free-icons' -import { meok, palette, surface , fontSize } from '@/design-system/tokens'; +import { meok, palette, surface, typographyRole } from '@/design-system/tokens'; import { STAY_TYPE, type Village } from '@/features/hanok-archive/types'; import { filterLabel } from '@/features/hanok-archive/filterLabels'; @@ -89,7 +89,7 @@ const SearchInput = styled.input` outline: none; background: none; font-family: inherit; - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; color: ${meok[900]}; &::placeholder { @@ -163,7 +163,7 @@ const RegionSelectControl = styled.div` `; const RegionSelectValue = styled.span` - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; line-height: 1; white-space: nowrap; `; @@ -185,7 +185,7 @@ const RegionSelect = styled.select` height: 42px; opacity: 0; font-family: inherit; - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; cursor: pointer; `; @@ -222,7 +222,7 @@ const Segment = styled.button<{ $active: boolean }>` background: transparent; border: none; padding: 9px 22px; - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; font-weight: ${({ $active }) => ($active ? 700 : 400)}; color: ${({ $active }) => ($active ? '#ffffff' : meok[700])}; cursor: pointer; @@ -273,7 +273,7 @@ const BadgeHeader = styled.div` display: flex; align-items: center; gap: 6px; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 600; letter-spacing: 0.04em; color: ${meok[700]}; @@ -314,7 +314,7 @@ const BadgeChip = styled.button<{ $active: boolean; $variant?: 'cheongrok' | 'ju return `linear-gradient(135deg, ${palette.juhong[500]} 0%, ${palette.juhong[700]} 100%)`; }}; color: ${({ $active }) => ($active ? '#ffffff' : meok[700])}; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: ${({ $active }) => ($active ? 600 : 400)}; padding: 6px 14px; border-radius: 9999px; @@ -342,7 +342,7 @@ const BadgeChip = styled.button<{ $active: boolean; $variant?: 'cheongrok' | 'ju const ResetBtn = styled.button` background: transparent; color: ${meok[500]}; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 500; padding: 4px 10px; border-radius: 9999px; diff --git a/src/features/hanok-archive/components/HanokArchiveIntroHeading.tsx b/src/features/hanok-archive/components/HanokArchiveIntroHeading.tsx index fac021fe..6503e199 100644 --- a/src/features/hanok-archive/components/HanokArchiveIntroHeading.tsx +++ b/src/features/hanok-archive/components/HanokArchiveIntroHeading.tsx @@ -2,12 +2,12 @@ import React from 'react'; import styled from '@emotion/styled'; -import { fontSize, lightPalette, meok } from '@/design-system/tokens'; +import { lightPalette, meok, typographyRole } from '@/design-system/tokens'; const Kicker = styled.p` margin: 0 0 16px; color: ${meok[500]}; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 600; letter-spacing: 0.02em; @@ -18,6 +18,7 @@ const Kicker = styled.p` const Title = styled.h1` + /* ponytail: 예외 — Dohyun 폰트 아카이브 인트로 타이틀, 브랜드 감성 의도적 크기 */ --section-heading-size: clamp(24px,3.2vw,36px); width: fit-content; margin: 0 0 14px; diff --git a/src/features/hanok-archive/components/HanokAudioPlayer.tsx b/src/features/hanok-archive/components/HanokAudioPlayer.tsx index 87410613..39158525 100644 --- a/src/features/hanok-archive/components/HanokAudioPlayer.tsx +++ b/src/features/hanok-archive/components/HanokAudioPlayer.tsx @@ -1,438 +1,438 @@ -'use client'; - -import React, { useState, useRef, useEffect } from 'react'; -import styled from '@emotion/styled'; -import { HugeiconsIcon } from '@hugeicons/react' -import { HeadphonesIcon, PlayIcon, PauseIcon, RotateCcwIcon, FileTextIcon, ChevronDownIcon, ChevronUpIcon } from '@hugeicons/core-free-icons' - -import type { SorimaruStory } from '../hooks/useHanokSorimaru'; -import { palette, meok, surface, fontSize } from '@/design-system/tokens'; - -interface HanokAudioPlayerProps { - stories: SorimaruStory[]; - hanokName: string; -} - -export default function HanokAudioPlayer({ stories, hanokName }: HanokAudioPlayerProps) { - const [currentIndex, setCurrentIndex] = useState(0); - const [isPlaying, setIsPlaying] = useState(false); - const [currentTime, setCurrentTime] = useState(0); - const [duration, setDuration] = useState(0); - const [showScript, setShowScript] = useState(false); - const audioRef = useRef(null); - - const currentStory = stories[currentIndex]; - - useEffect(() => { - setIsPlaying(false); - setCurrentTime(0); - setDuration(currentStory?.durationSeconds || 0); - }, [currentIndex, currentStory]); - - if (!stories || stories.length === 0) return null; - - const togglePlay = () => { - if (!audioRef.current) return; - if (isPlaying) { - audioRef.current.pause(); - setIsPlaying(false); - } else { - audioRef.current.play().then(() => setIsPlaying(true)).catch(() => setIsPlaying(false)); - } - }; - - const handleTimeUpdate = () => { - if (audioRef.current) { - setCurrentTime(audioRef.current.currentTime); - if (audioRef.current.duration && !isNaN(audioRef.current.duration)) { - setDuration(audioRef.current.duration); - } - } - }; - - const handleSeek = (e: React.ChangeEvent) => { - const time = Number(e.target.value); - if (audioRef.current) { - audioRef.current.currentTime = time; - setCurrentTime(time); - } - }; - - const handleRestart = () => { - if (audioRef.current) { - audioRef.current.currentTime = 0; - setCurrentTime(0); - audioRef.current.play().then(() => setIsPlaying(true)).catch(() => {}); - } - }; - - const formatSeconds = (sec: number) => { - const m = Math.floor(sec / 60); - const s = Math.floor(sec % 60); - return `${m}:${s < 10 ? '0' : ''}${s}`; - }; - - return ( - - - ); -} - -const Container = styled.div` - background: #f5f5f4; - border: none; - box-shadow: none; - border-radius: 20px; - padding: 22px 24px; - margin-top: 16px; - margin-bottom: 24px; - - [data-theme='dark'] & { - background: ${surface.dark.surface}; - } -`; - -const HeaderRow = styled.div` - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 12px; -`; - -const BadgeBox = styled.div` - display: inline-flex; - align-items: center; - gap: 6px; - background: ${palette.jangmi[50]}; - padding: 4px 10px; - border-radius: 9999px; - border: none; - color: ${palette.jangmi[700]}; - - svg { - color: ${palette.jangmi[700]}; - } - - [data-theme='dark'] & { - background: rgba(255, 42, 133, 0.16); - color: ${palette.jangmi[400]}; - - svg { - color: ${palette.jangmi[400]}; - } - } -`; - -const BadgeText = styled.span` - font-size: ${fontSize.xs}; - font-weight: 700; - color: ${palette.jangmi[700]}; - - [data-theme='dark'] & { - color: ${palette.jangmi[400]}; - } -`; - -const QualityTag = styled.span` - font-size: ${fontSize.micro}; - font-weight: 500; - color: ${meok[500]}; - - [data-theme='dark'] & { - color: ${meok[400]}; - } -`; - -const StoryTitleBox = styled.div` - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - margin-bottom: 16px; - flex-wrap: wrap; -`; - -const StoryTitle = styled.h4` - font-size: ${fontSize.sm}; - font-weight: 600; - color: ${meok[900]}; - margin: 0; - line-height: 1.4; - - [data-theme='dark'] & { - color: ${meok[100]}; - } -`; - -const SelectorSelect = styled.select` - font-size: ${fontSize.xs}; - background: #ffffff; - border: none; - box-shadow: none; - border-radius: 8px; - padding: 6px 10px; - color: ${meok[700]}; - cursor: pointer; - - [data-theme='dark'] & { - background: ${surface.dark.app}; - color: ${meok[200]}; - } -`; - -const PlayerControls = styled.div` - display: flex; - align-items: center; - gap: 12px; -`; - -const PlayBtn = styled.button` - display: flex; - align-items: center; - justify-content: center; - width: 40px; - height: 40px; - border-radius: 50%; - background: ${palette.jangmi[500]}; - color: #ffffff; - border: none; - box-shadow: none; - cursor: pointer; - transition: transform 0.15s ease, background-color 0.2s ease; - flex-shrink: 0; - - &:hover { - background: ${palette.jangmi[700]}; - transform: scale(1.05); - } - - &:active { - transform: scale(0.96); - } - - [data-theme='dark'] & { - background: ${palette.jangmi[500]}; - &:hover { - background: ${palette.jangmi[400]}; - } - } -`; - -const SecondaryBtn = styled.button` - display: flex; - align-items: center; - justify-content: center; - width: 32px; - height: 32px; - border-radius: 50%; - background: transparent; - color: ${meok[500]}; - border: none; - box-shadow: none; - cursor: pointer; - transition: all 0.2s ease; - flex-shrink: 0; - - &:hover { - background: rgba(0, 0, 0, 0.05); - color: ${meok[900]}; - } - - [data-theme='dark'] & { - color: ${meok[400]}; - &:hover { - background: rgba(255, 255, 255, 0.1); - color: ${meok[100]}; - } - } -`; - -const TimeDisplay = styled.span` - font-size: ${fontSize.xs}; - font-weight: 500; - font-variant-numeric: tabular-nums; - color: ${meok[500]}; - min-width: 34px; - text-align: center; - - [data-theme='dark'] & { - color: ${meok[400]}; - } -`; - -const ProgressBarWrapper = styled.div` - flex: 1; - display: flex; - align-items: center; -`; - -const ProgressBar = styled.input` - width: 100%; - height: 6px; - border-radius: 6px; - appearance: none; - background: rgba(0, 0, 0, 0.08); - outline: none; - cursor: pointer; - border: none; - - &::-webkit-slider-thumb { - appearance: none; - width: 12px; - height: 12px; - border-radius: 50%; - background: ${palette.cheongrok[500]}; - cursor: pointer; - transition: transform 0.1s ease; - border: none; - } - - &::-webkit-slider-thumb:hover { - transform: scale(1.25); - } - - [data-theme='dark'] & { - background: rgba(255, 255, 255, 0.12); - } -`; - -const ScriptSection = styled.div` - margin-top: 14px; - padding-top: 12px; - border: none; -`; - -const ScriptToggleBtn = styled.button` - display: flex; - align-items: center; - justify-content: space-between; - width: 100%; - background: none; - border: none; - box-shadow: none; - padding: 4px 0; - font-size: ${fontSize.xs}; - font-weight: 600; - color: ${meok[700]}; - cursor: pointer; - - &:hover { - color: ${palette.jangmi[700]}; - } - - [data-theme='dark'] & { - color: ${meok[200]}; - &:hover { - color: ${palette.jangmi[400]}; - } - } -`; - -const ScriptToggleLeft = styled.div` - display: flex; - align-items: center; - gap: 6px; -`; - -const ScriptContent = styled.div` - margin-top: 10px; - padding: 16px; - background: #ffffff; - border-radius: 12px; - border: none; - box-shadow: none; - max-height: 180px; - overflow-y: auto; - font-size: ${fontSize.xs}; - line-height: 1.7; - color: ${meok[700]}; - word-break: keep-all; - - p { - margin: 0 0 8px; - &:last-child { - margin-bottom: 0; - } - } - - [data-theme='dark'] & { - background: ${surface.dark.app}; - color: ${meok[200]}; - } -`; +'use client'; + +import React, { useState, useRef, useEffect } from 'react'; +import styled from '@emotion/styled'; +import { HugeiconsIcon } from '@hugeicons/react' +import { HeadphonesIcon, PlayIcon, PauseIcon, RotateCcwIcon, FileTextIcon, ChevronDownIcon, ChevronUpIcon } from '@hugeicons/core-free-icons' + +import type { SorimaruStory } from '../hooks/useHanokSorimaru'; +import { palette, meok, surface, typographyRole } from '@/design-system/tokens'; + +interface HanokAudioPlayerProps { + stories: SorimaruStory[]; + hanokName: string; +} + +export default function HanokAudioPlayer({ stories, hanokName }: HanokAudioPlayerProps) { + const [currentIndex, setCurrentIndex] = useState(0); + const [isPlaying, setIsPlaying] = useState(false); + const [currentTime, setCurrentTime] = useState(0); + const [duration, setDuration] = useState(0); + const [showScript, setShowScript] = useState(false); + const audioRef = useRef(null); + + const currentStory = stories[currentIndex]; + + useEffect(() => { + setIsPlaying(false); + setCurrentTime(0); + setDuration(currentStory?.durationSeconds || 0); + }, [currentIndex, currentStory]); + + if (!stories || stories.length === 0) return null; + + const togglePlay = () => { + if (!audioRef.current) return; + if (isPlaying) { + audioRef.current.pause(); + setIsPlaying(false); + } else { + audioRef.current.play().then(() => setIsPlaying(true)).catch(() => setIsPlaying(false)); + } + }; + + const handleTimeUpdate = () => { + if (audioRef.current) { + setCurrentTime(audioRef.current.currentTime); + if (audioRef.current.duration && !isNaN(audioRef.current.duration)) { + setDuration(audioRef.current.duration); + } + } + }; + + const handleSeek = (e: React.ChangeEvent) => { + const time = Number(e.target.value); + if (audioRef.current) { + audioRef.current.currentTime = time; + setCurrentTime(time); + } + }; + + const handleRestart = () => { + if (audioRef.current) { + audioRef.current.currentTime = 0; + setCurrentTime(0); + audioRef.current.play().then(() => setIsPlaying(true)).catch(() => {}); + } + }; + + const formatSeconds = (sec: number) => { + const m = Math.floor(sec / 60); + const s = Math.floor(sec % 60); + return `${m}:${s < 10 ? '0' : ''}${s}`; + }; + + return ( + + + ); +} + +const Container = styled.div` + background: #f5f5f4; + border: none; + box-shadow: none; + border-radius: 20px; + padding: 22px 24px; + margin-top: 16px; + margin-bottom: 24px; + + [data-theme='dark'] & { + background: ${surface.dark.surface}; + } +`; + +const HeaderRow = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 12px; +`; + +const BadgeBox = styled.div` + display: inline-flex; + align-items: center; + gap: 6px; + background: ${palette.jangmi[50]}; + padding: 4px 10px; + border-radius: 9999px; + border: none; + color: ${palette.jangmi[700]}; + + svg { + color: ${palette.jangmi[700]}; + } + + [data-theme='dark'] & { + background: rgba(255, 42, 133, 0.16); + color: ${palette.jangmi[400]}; + + svg { + color: ${palette.jangmi[400]}; + } + } +`; + +const BadgeText = styled.span` + font-size: ${typographyRole.label.fontSize}; + font-weight: 700; + color: ${palette.jangmi[700]}; + + [data-theme='dark'] & { + color: ${palette.jangmi[400]}; + } +`; + +const QualityTag = styled.span` + font-size: ${typographyRole.caption.fontSize}; + font-weight: 500; + color: ${meok[500]}; + + [data-theme='dark'] & { + color: ${meok[400]}; + } +`; + +const StoryTitleBox = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 16px; + flex-wrap: wrap; +`; + +const StoryTitle = styled.h4` + font-size: ${typographyRole.bodySm.fontSize}; + font-weight: 600; + color: ${meok[900]}; + margin: 0; + line-height: 1.4; + + [data-theme='dark'] & { + color: ${meok[100]}; + } +`; + +const SelectorSelect = styled.select` + font-size: ${typographyRole.label.fontSize}; + background: #ffffff; + border: none; + box-shadow: none; + border-radius: 8px; + padding: 6px 10px; + color: ${meok[700]}; + cursor: pointer; + + [data-theme='dark'] & { + background: ${surface.dark.app}; + color: ${meok[200]}; + } +`; + +const PlayerControls = styled.div` + display: flex; + align-items: center; + gap: 12px; +`; + +const PlayBtn = styled.button` + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + border-radius: 50%; + background: ${palette.jangmi[500]}; + color: #ffffff; + border: none; + box-shadow: none; + cursor: pointer; + transition: transform 0.15s ease, background-color 0.2s ease; + flex-shrink: 0; + + &:hover { + background: ${palette.jangmi[700]}; + transform: scale(1.05); + } + + &:active { + transform: scale(0.96); + } + + [data-theme='dark'] & { + background: ${palette.jangmi[500]}; + &:hover { + background: ${palette.jangmi[400]}; + } + } +`; + +const SecondaryBtn = styled.button` + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: 50%; + background: transparent; + color: ${meok[500]}; + border: none; + box-shadow: none; + cursor: pointer; + transition: all 0.2s ease; + flex-shrink: 0; + + &:hover { + background: rgba(0, 0, 0, 0.05); + color: ${meok[900]}; + } + + [data-theme='dark'] & { + color: ${meok[400]}; + &:hover { + background: rgba(255, 255, 255, 0.1); + color: ${meok[100]}; + } + } +`; + +const TimeDisplay = styled.span` + font-size: ${typographyRole.label.fontSize}; + font-weight: 500; + font-variant-numeric: tabular-nums; + color: ${meok[500]}; + min-width: 34px; + text-align: center; + + [data-theme='dark'] & { + color: ${meok[400]}; + } +`; + +const ProgressBarWrapper = styled.div` + flex: 1; + display: flex; + align-items: center; +`; + +const ProgressBar = styled.input` + width: 100%; + height: 6px; + border-radius: 6px; + appearance: none; + background: rgba(0, 0, 0, 0.08); + outline: none; + cursor: pointer; + border: none; + + &::-webkit-slider-thumb { + appearance: none; + width: 12px; + height: 12px; + border-radius: 50%; + background: ${palette.cheongrok[500]}; + cursor: pointer; + transition: transform 0.1s ease; + border: none; + } + + &::-webkit-slider-thumb:hover { + transform: scale(1.25); + } + + [data-theme='dark'] & { + background: rgba(255, 255, 255, 0.12); + } +`; + +const ScriptSection = styled.div` + margin-top: 14px; + padding-top: 12px; + border: none; +`; + +const ScriptToggleBtn = styled.button` + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + background: none; + border: none; + box-shadow: none; + padding: 4px 0; + font-size: ${typographyRole.label.fontSize}; + font-weight: 600; + color: ${meok[700]}; + cursor: pointer; + + &:hover { + color: ${palette.jangmi[700]}; + } + + [data-theme='dark'] & { + color: ${meok[200]}; + &:hover { + color: ${palette.jangmi[400]}; + } + } +`; + +const ScriptToggleLeft = styled.div` + display: flex; + align-items: center; + gap: 6px; +`; + +const ScriptContent = styled.div` + margin-top: 10px; + padding: 16px; + background: #ffffff; + border-radius: 12px; + border: none; + box-shadow: none; + max-height: 180px; + overflow-y: auto; + font-size: ${typographyRole.label.fontSize}; + line-height: 1.7; + color: ${meok[700]}; + word-break: keep-all; + + p { + margin: 0 0 8px; + &:last-child { + margin-bottom: 0; + } + } + + [data-theme='dark'] & { + background: ${surface.dark.app}; + color: ${meok[200]}; + } +`; diff --git a/src/features/hanok-archive/components/HanokDogamDetailModal.tsx b/src/features/hanok-archive/components/HanokDogamDetailModal.tsx index a2d7805e..02dbbcaf 100644 --- a/src/features/hanok-archive/components/HanokDogamDetailModal.tsx +++ b/src/features/hanok-archive/components/HanokDogamDetailModal.tsx @@ -8,7 +8,7 @@ import { ScrollTrigger } from 'gsap/ScrollTrigger'; import { HugeiconsIcon } from '@hugeicons/react' import { ArrowRight01Icon, Award01Icon, BookOpen01Icon, Bookmark01Icon, Building01Icon, Calendar01Icon, Cancel01Icon, Car01Icon, CheckIcon, ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon, Clock01Icon, Copy01Icon, Flower01Icon, GlobeIcon, Image02Icon, InformationCircleIcon, LandmarkIcon, Layers01Icon, Leaf01Icon, MapPinIcon, Navigation01Icon, PhoneIcon, SnowflakeIcon, SparklesIcon, SproutIcon, WindIcon, ZoomInIcon } from '@hugeicons/core-free-icons' import { inferStructureTags, extractHeritageGrade, inferSeasonTags } from '@/features/hanok-archive/utils/villageInsights'; -import { meok, palette, surface , fontSize } from '@/design-system/tokens'; +import { palette, typographyRole } from '@/design-system/tokens'; import { useBookmarkStore } from '@/features/map/hooks/useBookmarkStore'; import type { Village } from '@/features/hanok-archive/types'; import { filterLabel } from '@/features/hanok-archive/filterLabels'; @@ -706,7 +706,7 @@ const NaverDirectionsBtn = styled.a` border-radius: 9999px; background: ${palette.juhong[500]}; color: #ffffff; - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; font-weight: 600; display: flex; align-items: center; diff --git a/src/features/hanok-archive/components/HanokSideIndex.tsx b/src/features/hanok-archive/components/HanokSideIndex.tsx index 5e2f1538..69369d9f 100644 --- a/src/features/hanok-archive/components/HanokSideIndex.tsx +++ b/src/features/hanok-archive/components/HanokSideIndex.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useRef, useState } from 'react'; import styled from '@emotion/styled'; -import { meok, lightPalette } from '@/design-system/tokens'; +import { meok, lightPalette, typographyRole } from '@/design-system/tokens'; import { HANOK_REVEAL_SECTIONS } from '@/features/hanok-archive/hanokSectionReveal'; @@ -48,7 +48,7 @@ const IndexItem = styled.button<{ $active: boolean }>` cursor: pointer; text-align: left; font-family: var(--font-hanok); - font-size: 12.5px; + font-size: ${typographyRole.label.fontSize}; line-height: 1.35; font-weight: ${({ $active }) => ($active ? 700 : 400)}; letter-spacing: -0.01em; diff --git a/src/features/hanok-archive/components/HanokStayDetailModal.tsx b/src/features/hanok-archive/components/HanokStayDetailModal.tsx index 2c3ff4ba..1512ddb1 100644 --- a/src/features/hanok-archive/components/HanokStayDetailModal.tsx +++ b/src/features/hanok-archive/components/HanokStayDetailModal.tsx @@ -4,7 +4,7 @@ import React, { useMemo, useState, useEffect } from 'react'; import { AnimatePresence } from 'framer-motion'; import { HugeiconsIcon } from '@hugeicons/react' import { Bookmark01Icon, Cancel01Icon, Car01Icon, ChevronLeftIcon, ChevronRightIcon, Clock01Icon, Coffee01Icon, ExternalLinkIcon, Flower01Icon, GlobeIcon, Home01Icon, Image02Icon, Layers01Icon, Leaf01Icon, MapPinIcon, Navigation01Icon, PhoneIcon, SnowflakeIcon, SparklesIcon, SproutIcon, WindIcon, ZoomInIcon } from '@hugeicons/core-free-icons' -import { meok, palette, surface , fontSize } from '@/design-system/tokens'; +import { meok, surface, typographyRole } from '@/design-system/tokens'; import { useBookmarkStore } from '@/features/map/hooks/useBookmarkStore'; import type { Village } from '@/features/hanok-archive/types'; import { filterLabel } from '@/features/hanok-archive/filterLabels'; @@ -505,7 +505,7 @@ const DirectBookingButton = styled.a` border-radius: 9999px; background: var(--color-action-secondary); color: #ffffff; - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; font-weight: 600; display: flex; align-items: center; @@ -526,7 +526,7 @@ const MapGuideBtn = styled.a` border-radius: 9999px; background: #f5f5f4; color: ${meok[900]}; - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; font-weight: 600; display: flex; align-items: center; diff --git a/src/features/hanok-archive/components/KCultureThemeFeed.tsx b/src/features/hanok-archive/components/KCultureThemeFeed.tsx index b158438e..1f712e82 100644 --- a/src/features/hanok-archive/components/KCultureThemeFeed.tsx +++ b/src/features/hanok-archive/components/KCultureThemeFeed.tsx @@ -6,7 +6,7 @@ import { keyframes } from '@emotion/react'; import { motion, LayoutGroup, AnimatePresence } from 'framer-motion'; import { HugeiconsIcon } from '@hugeicons/react' import { ChevronLeftIcon, ChevronRightIcon, ClapperboardIcon, ExternalLinkIcon, Film01Icon, HashtagIcon, HeartIcon, MapPinIcon, Music02Icon, SparklesIcon } from '@hugeicons/core-free-icons' -import { meok, lightPalette, surface, fontSize } from '@/design-system/tokens'; +import { meok, lightPalette, typographyRole, mq } from '@/design-system/tokens'; import SectionHeader from '@/features/hanok-archive/components/SectionHeader'; import { useKCultureThemes } from '@/features/hanok-archive/hooks/useKCultureThemes'; import type { ScreenHanokItem, ScreenHanokMediaType } from '@/features/hanok-archive/services/screenHanok.service'; @@ -38,7 +38,7 @@ const FilterChip = styled.button<{ $active: boolean }>` border: 1px solid ${({ $active }) => ($active ? lightPalette.juhong[500] : 'rgba(78,89,104,0.16)')}; background: ${({ $active }) => ($active ? lightPalette.juhong[500] : 'transparent')}; color: ${({ $active }) => ($active ? '#ffffff' : meok[700])}; - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; font-weight: 600; cursor: pointer; transition: background 0.18s ease, border-color 0.18s ease, color 0.18s ease; @@ -59,11 +59,11 @@ const BentoGrid = styled.div` gap: 7px; align-items: stretch; - @media (min-width: 900px) { + ${mq.laptop} { grid-template-columns: 55fr 45fr; } - @media (max-width: 640px) { + ${mq.mobile} { grid-template-columns: 1fr; } `; @@ -154,7 +154,7 @@ const MediaBadge = styled.div` background: rgba(0, 0, 0, 0.55); backdrop-filter: blur(8px); color: #ffffff; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 600; letter-spacing: -0.01em; `; @@ -187,12 +187,12 @@ const HeroBody = styled.div` const HeroWorkTitle = styled.p` font-family: var(--font-hanok); - font-size: clamp(22px, 3.5vw, 30px); - font-weight: 700; + font-size: ${typographyRole.sectionTitle.fontSize}; + font-weight: ${typographyRole.sectionTitle.fontWeight}; color: #ffffff; margin: 0 0 5px; - line-height: 1.2; - letter-spacing: -0.03em; + line-height: ${typographyRole.sectionTitle.lineHeight}; + letter-spacing: ${typographyRole.sectionTitle.letterSpacing}; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; @@ -205,12 +205,12 @@ const HeroNameRow = styled.div` gap: 4px; margin-bottom: 10px; color: rgba(255,255,255,0.78); - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; font-weight: 500; `; const HeroSubtitle = styled.p` - font-size: 12.5px; + font-size: ${typographyRole.label.fontSize}; line-height: 1.5; color: rgba(255,255,255,0.82); margin: 0 0 12px; @@ -305,7 +305,7 @@ const ThumbMeta = styled.div` const ThumbWorkTitle = styled.p` font-family: var(--font-hanok); - font-size: 12px; + font-size: ${typographyRole.label.fontSize}; font-weight: 700; color: #ffffff; margin: 0 0 2px; @@ -432,7 +432,7 @@ const EmptyNote = styled.p` grid-column: 1 / -1; padding: 48px 0; text-align: center; - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; color: ${meok[500]}; `; @@ -482,7 +482,7 @@ const NavBtn = styled.button<{ $disabled: boolean }>` `; const NavCounter = styled.span` - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; font-weight: 600; color: ${meok[500]}; font-variant-numeric: tabular-nums; @@ -495,7 +495,7 @@ const NavCounter = styled.span` `; const ViewAllLink = styled.button` - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; font-weight: 600; color: ${lightPalette.juhong[500]}; background: none; diff --git a/src/features/hanok-archive/components/Pagination.tsx b/src/features/hanok-archive/components/Pagination.tsx index b2a4ea98..6a295d53 100644 --- a/src/features/hanok-archive/components/Pagination.tsx +++ b/src/features/hanok-archive/components/Pagination.tsx @@ -2,7 +2,7 @@ import React from 'react'; import styled from '@emotion/styled'; -import { meok, fontSize } from '@/design-system/tokens'; +import { meok, typographyRole } from '@/design-system/tokens'; const Wrapper = styled.div` position: relative; @@ -20,7 +20,7 @@ const PageBtn = styled.button<{ $active?: boolean; $disabled?: boolean }>` z-index: 1; background: ${({ $active }) => ($active ? meok[900] : '#ffffff')}; color: ${({ $active }) => ($active ? '#ffffff' : meok[700])}; - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; font-weight: ${({ $active }) => ($active ? 500 : 400)}; min-width: 40px; height: 40px; @@ -58,7 +58,7 @@ const PageBtn = styled.button<{ $active?: boolean; $disabled?: boolean }>` const Dots = styled.span` color: ${meok[400]}; padding: 0 4px; - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; [data-theme='dark'] & { color: ${meok[500]}; diff --git a/src/features/hanok-archive/components/PolaroidCard.tsx b/src/features/hanok-archive/components/PolaroidCard.tsx index 5418c4aa..c2d96be5 100644 --- a/src/features/hanok-archive/components/PolaroidCard.tsx +++ b/src/features/hanok-archive/components/PolaroidCard.tsx @@ -1,323 +1,324 @@ -'use client'; - -import React from 'react'; -import styled from '@emotion/styled'; -import { keyframes } from '@emotion/react'; -import { motion } from 'framer-motion'; -import { transientProps } from '@/design-system/styled'; - -import { HugeiconsIcon } from '@hugeicons/react' -import { Home01Icon, ArrowRight01Icon } from '@hugeicons/core-free-icons' -import type { Village } from '@/features/hanok-archive/types'; -import { filterLabel } from '@/features/hanok-archive/filterLabels'; -import { lightPalette, meok, surface, ringShadow, fontSize } from '@/design-system/tokens'; - - -const ROTATIONS = [-4.2, 2.8, -2.1, 3.5]; - -const Wrapper = styled(motion.article, transientProps)<{ $rotate: number }>` - display: flex; - flex-direction: column; - cursor: pointer; - user-select: none; - position: relative; - transform-origin: center bottom; - will-change: transform; -`; - -const Frame = styled.div` - background: #ffffff; - padding: 12px 12px 60px; - box-shadow: ${ringShadow.light.card}; - border-radius: 2px; - position: relative; - overflow: visible; - - [data-theme='dark'] & { - background: ${surface.dark.surface}; - border: 1px solid rgba(255, 255, 255, 0.08); - box-shadow: ${ringShadow.dark.card}; - } -`; - - - - - - - - - - - - - - - - - - - - -const PhotoArea = styled.div` - position: relative; - width: 100%; - padding-top: 100%; - overflow: hidden; - background: ${lightPalette.juhong[50]}; -`; - -const Photo = styled(motion.div, transientProps)<{ $bg: string | null }>` - position: absolute; - inset: 0; - ${({ $bg }) => - $bg - ? `background-image: url("${$bg}"); background-size: cover; background-position: center;` - : `background: linear-gradient(135deg, #e8ded2 0%, #d4c9bb 100%);`} -`; - -const shimmer = keyframes` - 0% { background-position: -400px 0; } - 100% { background-position: 400px 0; } -`; - -const PhotoSkeleton = styled.div` - position: absolute; - inset: 0; - background: linear-gradient(90deg, ${lightPalette.juhong[50]} 25%, #fff8f0 50%, ${lightPalette.juhong[50]} 75%); - background-size: 800px 100%; - animation: ${shimmer} 1.4s ease-in-out infinite; - - @media (prefers-reduced-motion: reduce) { - animation: none; - } -`; - -const NoImageLabel = styled.div` - position: absolute; - inset: 0; - display: flex; - align-items: center; - justify-content: center; - font-size: ${fontSize['3xl']}; - opacity: 0.25; -`; - -const HandWritingCaption = styled.div` - position: absolute; - bottom: 0; - left: 0; - right: 0; - height: 60px; - display: flex; - align-items: center; - justify-content: center; - padding: 0 12px; -`; - -const HandWritingText = styled.p` - font-family: 'GeuriunXGukhanbakSingoyangI', var(--font-hanok); - font-size: ${fontSize.xl}; - font-weight: 400; - color: ${meok[900]}; - margin: 0; - text-align: center; - letter-spacing: 0.04em; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - - [data-theme='dark'] & { - color: ${meok[100]}; - } -`; - -const TapeStrip = styled.div<{ $left?: boolean }>` - position: absolute; - top: -11px; - ${({ $left }) => ($left ? 'left: 20px;' : 'right: 20px;')} - width: 44px; - height: 22px; - background: ${lightPalette.juhong[300]}47; - border-radius: 2px; - transform: rotate(${({ $left }) => ($left ? '-5deg' : '4deg')}); - z-index: 10; - - &::before { - content: ''; - position: absolute; - inset: 3px 0; - background: repeating-linear-gradient( - 90deg, - transparent, - transparent 4px, - rgba(255,255,255,0.25) 4px, - rgba(255,255,255,0.25) 5px - ); - } -`; - - -const OutsideMeta = styled.div` - margin-top: 16px; - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 6px; - padding: 0 4px; -`; - -const OutsideTitle = styled.h3` - font-family: var(--font-hanok); - font-size: ${fontSize.base}; - font-weight: 300; - color: ${meok[900]}; - margin: 0; - letter-spacing: -0.02em; - line-height: 1.3; - - [data-theme='dark'] & { - color: ${meok[100]}; - } -`; - -const OutsideBadges = styled.div` - display: flex; - flex-wrap: wrap; - gap: 6px; - - span { - font-family: var(--font-hanok); - font-size: ${fontSize.xs}; - font-weight: 500; - color: ${lightPalette.juhong[500]}; - background: rgba(78, 89, 104, 0.06); - padding: 3px 10px; - border-radius: 9999px; - - [data-theme='dark'] & { - color: ${lightPalette.juhong[400]}; - background: rgba(255, 255, 255, 0.08); - } - } -`; - -const DetailButton = styled.div` - display: inline-flex; - align-items: center; - gap: 4px; - margin-top: 4px; - font-family: var(--font-hanok); - font-size: ${fontSize.xs}; - font-weight: 400; - color: ${meok[900]}; - transition: color 0.2s ease, transform 0.2s ease; - - .polaroid-card:hover & { - color: ${lightPalette.juhong[500]}; - transform: translateX(3px); - } - - [data-theme='dark'] & { - color: ${meok[100]}; - } -`; - -interface PolaroidCardProps { - village: Village; - index: number; - customHandText?: string; - showOutsideMeta?: boolean; - onClick?: (village: Village) => void; - isImageLoading?: boolean; - - enableTape?: boolean; -} - -export default function PolaroidCard({ - village, - index, - customHandText, - showOutsideMeta = false, - onClick, - isImageLoading = false, - enableTape = true, -}: PolaroidCardProps) { - const rotate = ROTATIONS[index % ROTATIONS.length]; - const showTape = enableTape && index % 2 === 0; - const tapeLeft = index % 4 < 2; - - const regionName = village.region || '한옥'; - const handText = customHandText || `고즈넉한 ${regionName}에서`; - - return ( - onClick?.(village)} - role="button" - tabIndex={0} - aria-label={`${village.name} 자세히 보기`} - initial={{ opacity: 0, y: 40, rotate }} - whileInView={{ opacity: 1, y: 0, rotate }} - viewport={{ once: true, margin: '-40px' }} - transition={{ - delay: index * 0.09, - duration: 0.55, - ease: [0.25, 0.1, 0.25, 1], - }} - whileHover={{ - rotate: 0, - y: -12, - scale: 1.03, - zIndex: 10, - transition: { type: 'spring', stiffness: 300, damping: 20 }, - }} - whileTap={{ scale: 0.97 }} - onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === ' ') onClick?.(village); - }} - style={{ rotate }} - > - {showTape && } - - - - {isImageLoading ? ( - - ) : ( - <> - - {!village.hasImage && } - - )} - - - - {handText} - - - - {showOutsideMeta && ( - - {village.name} · {village.region} - - - #{filterLabel(village.type)} - {village.badges.slice(0, 2).map((b) => ( - #{filterLabel(b)} - ))} - - - - 자세히 보기 - - - )} - - ); -} +'use client'; + +import React from 'react'; +import styled from '@emotion/styled'; +import { keyframes } from '@emotion/react'; +import { motion } from 'framer-motion'; +import { transientProps } from '@/design-system/styled'; + +import { HugeiconsIcon } from '@hugeicons/react' +import { Home01Icon, ArrowRight01Icon } from '@hugeicons/core-free-icons' +import type { Village } from '@/features/hanok-archive/types'; +import { filterLabel } from '@/features/hanok-archive/filterLabels'; +import { lightPalette, meok, surface, ringShadow, fontSize, typographyRole } from '@/design-system/tokens'; + + +const ROTATIONS = [-4.2, 2.8, -2.1, 3.5]; + +const Wrapper = styled(motion.article, transientProps)<{ $rotate: number }>` + display: flex; + flex-direction: column; + cursor: pointer; + user-select: none; + position: relative; + transform-origin: center bottom; + will-change: transform; +`; + +const Frame = styled.div` + background: #ffffff; + padding: 12px 12px 60px; + box-shadow: ${ringShadow.light.card}; + border-radius: 2px; + position: relative; + overflow: visible; + + [data-theme='dark'] & { + background: ${surface.dark.surface}; + border: 1px solid rgba(255, 255, 255, 0.08); + box-shadow: ${ringShadow.dark.card}; + } +`; + + + + + + + + + + + + + + + + + + + + +const PhotoArea = styled.div` + position: relative; + width: 100%; + padding-top: 100%; + overflow: hidden; + background: ${lightPalette.juhong[50]}; +`; + +const Photo = styled(motion.div, transientProps)<{ $bg: string | null }>` + position: absolute; + inset: 0; + ${({ $bg }) => + $bg + ? `background-image: url("${$bg}"); background-size: cover; background-position: center;` + : `background: linear-gradient(135deg, #e8ded2 0%, #d4c9bb 100%);`} +`; + +const shimmer = keyframes` + 0% { background-position: -400px 0; } + 100% { background-position: 400px 0; } +`; + +const PhotoSkeleton = styled.div` + position: absolute; + inset: 0; + background: linear-gradient(90deg, ${lightPalette.juhong[50]} 25%, #fff8f0 50%, ${lightPalette.juhong[50]} 75%); + background-size: 800px 100%; + animation: ${shimmer} 1.4s ease-in-out infinite; + + @media (prefers-reduced-motion: reduce) { + animation: none; + } +`; + +const NoImageLabel = styled.div` + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + font-size: ${fontSize['3xl']}; + opacity: 0.25; +`; + +const HandWritingCaption = styled.div` + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 60px; + display: flex; + align-items: center; + justify-content: center; + padding: 0 12px; +`; + +const HandWritingText = styled.p` + font-family: 'GeuriunXGukhanbakSingoyangI', var(--font-hanok); + /* ponytail: 예외 — 폴라로이드 손글씨 캡션, 수기 폰트 감성 크기 */ + font-size: ${fontSize.xl}; + font-weight: 400; + color: ${meok[900]}; + margin: 0; + text-align: center; + letter-spacing: 0.04em; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + [data-theme='dark'] & { + color: ${meok[100]}; + } +`; + +const TapeStrip = styled.div<{ $left?: boolean }>` + position: absolute; + top: -11px; + ${({ $left }) => ($left ? 'left: 20px;' : 'right: 20px;')} + width: 44px; + height: 22px; + background: ${lightPalette.juhong[300]}47; + border-radius: 2px; + transform: rotate(${({ $left }) => ($left ? '-5deg' : '4deg')}); + z-index: 10; + + &::before { + content: ''; + position: absolute; + inset: 3px 0; + background: repeating-linear-gradient( + 90deg, + transparent, + transparent 4px, + rgba(255,255,255,0.25) 4px, + rgba(255,255,255,0.25) 5px + ); + } +`; + + +const OutsideMeta = styled.div` + margin-top: 16px; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 6px; + padding: 0 4px; +`; + +const OutsideTitle = styled.h3` + font-family: var(--font-hanok); + font-size: ${typographyRole.body.fontSize}; + font-weight: 300; + color: ${meok[900]}; + margin: 0; + letter-spacing: -0.02em; + line-height: 1.3; + + [data-theme='dark'] & { + color: ${meok[100]}; + } +`; + +const OutsideBadges = styled.div` + display: flex; + flex-wrap: wrap; + gap: 6px; + + span { + font-family: var(--font-hanok); + font-size: ${typographyRole.label.fontSize}; + font-weight: 500; + color: ${lightPalette.juhong[500]}; + background: rgba(78, 89, 104, 0.06); + padding: 3px 10px; + border-radius: 9999px; + + [data-theme='dark'] & { + color: ${lightPalette.juhong[400]}; + background: rgba(255, 255, 255, 0.08); + } + } +`; + +const DetailButton = styled.div` + display: inline-flex; + align-items: center; + gap: 4px; + margin-top: 4px; + font-family: var(--font-hanok); + font-size: ${typographyRole.label.fontSize}; + font-weight: 400; + color: ${meok[900]}; + transition: color 0.2s ease, transform 0.2s ease; + + .polaroid-card:hover & { + color: ${lightPalette.juhong[500]}; + transform: translateX(3px); + } + + [data-theme='dark'] & { + color: ${meok[100]}; + } +`; + +interface PolaroidCardProps { + village: Village; + index: number; + customHandText?: string; + showOutsideMeta?: boolean; + onClick?: (village: Village) => void; + isImageLoading?: boolean; + + enableTape?: boolean; +} + +export default function PolaroidCard({ + village, + index, + customHandText, + showOutsideMeta = false, + onClick, + isImageLoading = false, + enableTape = true, +}: PolaroidCardProps) { + const rotate = ROTATIONS[index % ROTATIONS.length]; + const showTape = enableTape && index % 2 === 0; + const tapeLeft = index % 4 < 2; + + const regionName = village.region || '한옥'; + const handText = customHandText || `고즈넉한 ${regionName}에서`; + + return ( + onClick?.(village)} + role="button" + tabIndex={0} + aria-label={`${village.name} 자세히 보기`} + initial={{ opacity: 0, y: 40, rotate }} + whileInView={{ opacity: 1, y: 0, rotate }} + viewport={{ once: true, margin: '-40px' }} + transition={{ + delay: index * 0.09, + duration: 0.55, + ease: [0.25, 0.1, 0.25, 1], + }} + whileHover={{ + rotate: 0, + y: -12, + scale: 1.03, + zIndex: 10, + transition: { type: 'spring', stiffness: 300, damping: 20 }, + }} + whileTap={{ scale: 0.97 }} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') onClick?.(village); + }} + style={{ rotate }} + > + {showTape && } + + + + {isImageLoading ? ( + + ) : ( + <> + + {!village.hasImage && } + + )} + + + + {handText} + + + + {showOutsideMeta && ( + + {village.name} · {village.region} + + + #{filterLabel(village.type)} + {village.badges.slice(0, 2).map((b) => ( + #{filterLabel(b)} + ))} + + + + 자세히 보기 + + + )} + + ); +} diff --git a/src/features/hanok-archive/components/QuickIndexBar.tsx b/src/features/hanok-archive/components/QuickIndexBar.tsx index d2974fd8..948f28d4 100644 --- a/src/features/hanok-archive/components/QuickIndexBar.tsx +++ b/src/features/hanok-archive/components/QuickIndexBar.tsx @@ -2,7 +2,7 @@ import React from 'react'; import styled from '@emotion/styled'; -import { meok, palette, surface , fontSize } from '@/design-system/tokens'; +import { meok, palette, typographyRole } from '@/design-system/tokens'; import { HugeiconsIcon } from '@hugeicons/react' import { BookOpen01Icon, Home01Icon, Sun01Icon, MapPinIcon } from '@hugeicons/core-free-icons' @@ -47,7 +47,7 @@ const IndexChip = styled.button` background: transparent; color: ${meok[700]}; font-family: inherit; - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; font-weight: 500; letter-spacing: -0.01em; cursor: pointer; @@ -75,7 +75,7 @@ const IndexChip = styled.button` @media (max-width: 640px) { padding: 6px 10px; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; } `; diff --git a/src/features/hanok-archive/components/SoriMaruBridgeCard.tsx b/src/features/hanok-archive/components/SoriMaruBridgeCard.tsx index 7e509faa..875d1dc9 100644 --- a/src/features/hanok-archive/components/SoriMaruBridgeCard.tsx +++ b/src/features/hanok-archive/components/SoriMaruBridgeCard.tsx @@ -1,213 +1,213 @@ -'use client'; - -import React from 'react'; -import styled from '@emotion/styled'; -import { HugeiconsIcon } from '@hugeicons/react' -import { HeadphonesIcon, ArrowRight01Icon, SparklesIcon } from '@hugeicons/core-free-icons' -import { useRouter } from 'next/navigation'; - -import type { AudioGuideStory } from '../hooks/useHanokAudioGuide'; -import { palette, meok, surface, fontSize } from '@/design-system/tokens'; - -interface SoriMaruBridgeCardProps { - stories: AudioGuideStory[]; - hanokName: string; -} - - - - - - -export default function SoriMaruBridgeCard({ stories, hanokName }: SoriMaruBridgeCardProps) { - const router = useRouter(); - - if (!stories || stories.length === 0) return null; - - const primaryStory = stories[0]; - - const handleNavigateToSoriMaru = () => { - - - router.push(`/sorimaru?stid=${encodeURIComponent(primaryStory.storyId)}`); - }; - - const formatSeconds = (sec: number) => { - const m = Math.floor(sec / 60); - const s = Math.floor(sec % 60); - return `${m}분 ${s > 0 ? `${s}초` : ''}`; - }; - - return ( - - - - - 소리마루 도슨트 연계 - - 소리마루 공식 도슨트 음원 - - - - - {primaryStory.audioTitle || `${hanokName} 건축 공간 해설`} - - - {primaryStory.durationSeconds > 0 && ( - 약 {formatSeconds(primaryStory.durationSeconds)} 소요 · - )} - 이 건축물의 역사와 공간 이야기는 ‘소리마루’에서 들을 수 있어요.{' '} - 고음질 음원에 대본까지 함께 볼 수 있습니다. - - - - - - 소리마루에서 도슨트 듣기 - - - - - ); -} - -const CardContainer = styled.div` - background: #f8f8f7; - border-radius: 20px; - padding: 20px 22px; - margin-top: 16px; - margin-bottom: 24px; - border: none; - box-shadow: none; - transition: background-color 0.2s ease; - - [data-theme='dark'] & { - background: ${surface.dark.surface}; - } -`; - -const HeaderRow = styled.div` - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 12px; -`; - -const BadgeBox = styled.div` - display: inline-flex; - align-items: center; - gap: 6px; - background: ${palette.jangmi[50]}; - color: ${palette.jangmi[700]}; - font-size: ${fontSize.xs}; - font-weight: 700; - padding: 4px 10px; - border-radius: 9999px; - - svg { - color: ${palette.jangmi[700]}; - } - - [data-theme='dark'] & { - background: rgba(255, 42, 133, 0.16); - color: ${palette.jangmi[400]}; - - svg { - color: ${palette.jangmi[400]}; - } - } -`; - -const OfficialTag = styled.span` - font-size: ${fontSize.micro}; - font-weight: 500; - color: ${meok[500]}; - - [data-theme='dark'] & { - color: ${meok[400]}; - } -`; - -const ContentBody = styled.div` - margin-bottom: 16px; -`; - -const StoryTitle = styled.h4` - font-size: ${fontSize.sm}; - font-weight: 700; - color: ${meok[900]}; - margin: 0 0 6px; - line-height: 1.4; - - [data-theme='dark'] & { - color: ${meok[100]}; - } -`; - -const GuideDesc = styled.p` - font-size: ${fontSize.xs}; - line-height: 1.6; - color: ${meok[700]}; - margin: 0; - word-break: keep-all; - - strong { - font-weight: 700; - color: ${palette.jangmi[700]}; - } - - [data-theme='dark'] & { - color: ${meok[200]}; - - strong { - color: ${palette.jangmi[400]}; - } - } -`; - -const DurationText = styled.span` - font-weight: 600; - color: ${meok[700]}; - - [data-theme='dark'] & { - color: ${meok[200]}; - } -`; - -const ActionRow = styled.div` - display: flex; - align-items: center; - justify-content: flex-end; -`; - -const ListenInSoriMaruBtn = styled.button` - display: inline-flex; - align-items: center; - gap: 8px; - height: 40px; - padding: 0 18px; - border-radius: 9999px; - background: ${palette.jangmi[500]}; - color: #ffffff; - font-size: ${fontSize.xs}; - font-weight: 600; - border: none; - cursor: pointer; - transition: all 0.2s ease; - - &:hover { - background: ${palette.jangmi[700]}; - transform: translateY(-1px); - } - - &:active { - transform: scale(0.98); - } - - [data-theme='dark'] & { - background: ${palette.jangmi[500]}; - &:hover { - background: ${palette.jangmi[400]}; - } - } -`; +'use client'; + +import React from 'react'; +import styled from '@emotion/styled'; +import { HugeiconsIcon } from '@hugeicons/react' +import { HeadphonesIcon, ArrowRight01Icon } from '@hugeicons/core-free-icons' +import { useRouter } from 'next/navigation'; + +import type { AudioGuideStory } from '../hooks/useHanokAudioGuide'; +import { palette, meok, surface, typographyRole } from '@/design-system/tokens'; + +interface SoriMaruBridgeCardProps { + stories: AudioGuideStory[]; + hanokName: string; +} + + + + + + +export default function SoriMaruBridgeCard({ stories, hanokName }: SoriMaruBridgeCardProps) { + const router = useRouter(); + + if (!stories || stories.length === 0) return null; + + const primaryStory = stories[0]; + + const handleNavigateToSoriMaru = () => { + + + router.push(`/sorimaru?stid=${encodeURIComponent(primaryStory.storyId)}`); + }; + + const formatSeconds = (sec: number) => { + const m = Math.floor(sec / 60); + const s = Math.floor(sec % 60); + return `${m}분 ${s > 0 ? `${s}초` : ''}`; + }; + + return ( + + + + + 소리마루 도슨트 연계 + + 소리마루 공식 도슨트 음원 + + + + + {primaryStory.audioTitle || `${hanokName} 건축 공간 해설`} + + + {primaryStory.durationSeconds > 0 && ( + 약 {formatSeconds(primaryStory.durationSeconds)} 소요 · + )} + 이 건축물의 역사와 공간 이야기는 ‘소리마루’에서 들을 수 있어요.{' '} + 고음질 음원에 대본까지 함께 볼 수 있습니다. + + + + + + 소리마루에서 도슨트 듣기 + + + + + ); +} + +const CardContainer = styled.div` + background: #f8f8f7; + border-radius: 20px; + padding: 20px 22px; + margin-top: 16px; + margin-bottom: 24px; + border: none; + box-shadow: none; + transition: background-color 0.2s ease; + + [data-theme='dark'] & { + background: ${surface.dark.surface}; + } +`; + +const HeaderRow = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 12px; +`; + +const BadgeBox = styled.div` + display: inline-flex; + align-items: center; + gap: 6px; + background: ${palette.jangmi[50]}; + color: ${palette.jangmi[700]}; + font-size: ${typographyRole.label.fontSize}; + font-weight: 700; + padding: 4px 10px; + border-radius: 9999px; + + svg { + color: ${palette.jangmi[700]}; + } + + [data-theme='dark'] & { + background: rgba(255, 42, 133, 0.16); + color: ${palette.jangmi[400]}; + + svg { + color: ${palette.jangmi[400]}; + } + } +`; + +const OfficialTag = styled.span` + font-size: ${typographyRole.caption.fontSize}; + font-weight: 500; + color: ${meok[500]}; + + [data-theme='dark'] & { + color: ${meok[400]}; + } +`; + +const ContentBody = styled.div` + margin-bottom: 16px; +`; + +const StoryTitle = styled.h4` + font-size: ${typographyRole.bodySm.fontSize}; + font-weight: 700; + color: ${meok[900]}; + margin: 0 0 6px; + line-height: 1.4; + + [data-theme='dark'] & { + color: ${meok[100]}; + } +`; + +const GuideDesc = styled.p` + font-size: ${typographyRole.label.fontSize}; + line-height: 1.6; + color: ${meok[700]}; + margin: 0; + word-break: keep-all; + + strong { + font-weight: 700; + color: ${palette.jangmi[700]}; + } + + [data-theme='dark'] & { + color: ${meok[200]}; + + strong { + color: ${palette.jangmi[400]}; + } + } +`; + +const DurationText = styled.span` + font-weight: 600; + color: ${meok[700]}; + + [data-theme='dark'] & { + color: ${meok[200]}; + } +`; + +const ActionRow = styled.div` + display: flex; + align-items: center; + justify-content: flex-end; +`; + +const ListenInSoriMaruBtn = styled.button` + display: inline-flex; + align-items: center; + gap: 8px; + height: 40px; + padding: 0 18px; + border-radius: 9999px; + background: ${palette.jangmi[500]}; + color: #ffffff; + font-size: ${typographyRole.label.fontSize}; + font-weight: 600; + border: none; + cursor: pointer; + transition: all 0.2s ease; + + &:hover { + background: ${palette.jangmi[700]}; + transform: translateY(-1px); + } + + &:active { + transform: scale(0.98); + } + + [data-theme='dark'] & { + background: ${palette.jangmi[500]}; + &:hover { + background: ${palette.jangmi[400]}; + } + } +`; diff --git a/src/features/hanok-archive/components/TranquilityGauge.tsx b/src/features/hanok-archive/components/TranquilityGauge.tsx index f8218cbd..50f20abd 100644 --- a/src/features/hanok-archive/components/TranquilityGauge.tsx +++ b/src/features/hanok-archive/components/TranquilityGauge.tsx @@ -1,257 +1,257 @@ -'use client'; - -import React from 'react'; -import styled from '@emotion/styled'; -import { HugeiconsIcon } from '@hugeicons/react' -import { Activity01Icon, Clock01Icon, ShieldCheckIcon, SparklesIcon } from '@hugeicons/core-free-icons' - -import type { TranquilityData } from '../hooks/useHanokTranquility'; -import { palette, meok, surface, fontSize } from '@/design-system/tokens'; - -interface TranquilityGaugeProps { - data: TranquilityData | null; - loading?: boolean; -} - -export default function TranquilityGauge({ data, loading }: TranquilityGaugeProps) { - if (loading) { - return ( - - - - - 관광 빅데이터 실시간 분석 중... - - - - ); - } - - if (!data) return null; - - return ( - - - - - 한국관광공사 DataLab 실시간 고즈넉 지수 - - {data.district} 권역 - - - - - {data.score} - / 100 - - - - - {data.level} - - - - {} - - - - - - - - 추천 골든타임: - {data.goldenHour} - - - - - - {data.advice} - - - ); -} - -const Container = styled.div` - background: #f5f5f4; - border: none; - box-shadow: none; - border-radius: 20px; - padding: 22px 24px; - margin-top: 16px; - margin-bottom: 24px; - - [data-theme='dark'] & { - background: ${surface.dark.surface}; - } -`; - -const HeaderRow = styled.div` - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 14px; -`; - -const BadgeBox = styled.div` - display: inline-flex; - align-items: center; - gap: 6px; - background: ${palette.juhong[50]}; - padding: 4px 10px; - border-radius: 9999px; - border: none; - - [data-theme='dark'] & { - background: rgba(255, 85, 0, 0.2); - } -`; - -const BadgeText = styled.span` - font-size: ${fontSize.xs}; - font-weight: 700; - color: ${palette.juhong[700]}; - - [data-theme='dark'] & { - color: ${palette.juhong[400]}; - } -`; - -const DistrictTag = styled.span` - font-size: ${fontSize.xs}; - font-weight: 500; - color: ${meok[500]}; - - [data-theme='dark'] & { - color: ${meok[400]}; - } -`; - -const MainRow = styled.div` - display: flex; - align-items: baseline; - justify-content: space-between; - margin-bottom: 10px; -`; - -const ScoreBox = styled.div` - display: flex; - align-items: baseline; - gap: 4px; -`; - -const ScoreNumber = styled.span` - font-family: var(--font-hanok); - font-size: ${fontSize['3xl']}; - font-weight: 700; - color: ${meok[900]}; - line-height: 1; - - [data-theme='dark'] & { - color: ${meok[100]}; - } -`; - -const ScoreMax = styled.span` - font-size: ${fontSize.xs}; - color: ${meok[400]}; -`; - -const LevelBadge = styled.div` - display: inline-flex; - align-items: center; - gap: 5px; - font-size: ${fontSize.xs}; - font-weight: 700; - padding: 5px 13px; - border-radius: 9999px; - border: none; - box-shadow: none; -`; - -const GaugeTrack = styled.div` - width: 100%; - height: 8px; - background: rgba(0, 0, 0, 0.06); - border-radius: 9999px; - overflow: hidden; - margin-bottom: 14px; - border: none; - - [data-theme='dark'] & { - background: rgba(255, 255, 255, 0.08); - } -`; - -const GaugeFill = styled.div` - height: 100%; - border-radius: 9999px; - transition: width 0.6s cubic-bezier(0.16, 1, 0.3, 1); -`; - -const InfoCardsRow = styled.div` - display: flex; - gap: 10px; - margin-bottom: 10px; - flex-wrap: wrap; -`; - -const InfoPill = styled.div` - display: inline-flex; - align-items: center; - gap: 6px; - font-size: ${fontSize.xs}; - color: ${meok[700]}; - background: #ffffff; - padding: 8px 14px; - border-radius: 10px; - border: none; - box-shadow: none; - - [data-theme='dark'] & { - background: ${surface.dark.app}; - color: ${meok[200]}; - } -`; - -const PillLabel = styled.span` - font-weight: 600; - color: ${meok[500]}; - - [data-theme='dark'] & { - color: ${meok[400]}; - } -`; - -const PillVal = styled.span` - font-weight: 700; - color: ${meok[900]}; - - [data-theme='dark'] & { - color: ${meok[100]}; - } -`; - -const AdviceText = styled.div` - display: flex; - align-items: flex-start; - gap: 8px; - font-size: ${fontSize.xs}; - line-height: 1.5; - color: ${meok[500]}; - background: rgba(0, 0, 0, 0.02); - padding: 10px 14px; - border-radius: 10px; - border: none; - box-shadow: none; - - [data-theme='dark'] & { - background: rgba(255, 255, 255, 0.04); - color: ${meok[200]}; - } -`; +'use client'; + +import React from 'react'; +import styled from '@emotion/styled'; +import { HugeiconsIcon } from '@hugeicons/react' +import { Activity01Icon, Clock01Icon, ShieldCheckIcon, SparklesIcon } from '@hugeicons/core-free-icons' + +import type { TranquilityData } from '../hooks/useHanokTranquility'; +import { palette, meok, surface, fontSize, typographyRole } from '@/design-system/tokens'; + +interface TranquilityGaugeProps { + data: TranquilityData | null; + loading?: boolean; +} + +export default function TranquilityGauge({ data, loading }: TranquilityGaugeProps) { + if (loading) { + return ( + + + + + 관광 빅데이터 실시간 분석 중... + + + + ); + } + + if (!data) return null; + + return ( + + + + + 한국관광공사 DataLab 실시간 고즈넉 지수 + + {data.district} 권역 + + + + + {data.score} + / 100 + + + + + {data.level} + + + + {} + + + + + + + + 추천 골든타임: + {data.goldenHour} + + + + + + {data.advice} + + + ); +} + +const Container = styled.div` + background: #f5f5f4; + border: none; + box-shadow: none; + border-radius: 20px; + padding: 22px 24px; + margin-top: 16px; + margin-bottom: 24px; + + [data-theme='dark'] & { + background: ${surface.dark.surface}; + } +`; + +const HeaderRow = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 14px; +`; + +const BadgeBox = styled.div` + display: inline-flex; + align-items: center; + gap: 6px; + background: ${palette.juhong[50]}; + padding: 4px 10px; + border-radius: 9999px; + border: none; + + [data-theme='dark'] & { + background: rgba(255, 85, 0, 0.2); + } +`; + +const BadgeText = styled.span` + font-size: ${typographyRole.label.fontSize}; + font-weight: 700; + color: ${palette.juhong[700]}; + + [data-theme='dark'] & { + color: ${palette.juhong[400]}; + } +`; + +const DistrictTag = styled.span` + font-size: ${typographyRole.label.fontSize}; + font-weight: 500; + color: ${meok[500]}; + + [data-theme='dark'] & { + color: ${meok[400]}; + } +`; + +const MainRow = styled.div` + display: flex; + align-items: baseline; + justify-content: space-between; + margin-bottom: 10px; +`; + +const ScoreBox = styled.div` + display: flex; + align-items: baseline; + gap: 4px; +`; + +const ScoreNumber = styled.span` + font-family: var(--font-hanok); + font-size: ${fontSize['3xl']}; + font-weight: 700; + color: ${meok[900]}; + line-height: 1; + + [data-theme='dark'] & { + color: ${meok[100]}; + } +`; + +const ScoreMax = styled.span` + font-size: ${typographyRole.label.fontSize}; + color: ${meok[400]}; +`; + +const LevelBadge = styled.div` + display: inline-flex; + align-items: center; + gap: 5px; + font-size: ${typographyRole.label.fontSize}; + font-weight: 700; + padding: 5px 13px; + border-radius: 9999px; + border: none; + box-shadow: none; +`; + +const GaugeTrack = styled.div` + width: 100%; + height: 8px; + background: rgba(0, 0, 0, 0.06); + border-radius: 9999px; + overflow: hidden; + margin-bottom: 14px; + border: none; + + [data-theme='dark'] & { + background: rgba(255, 255, 255, 0.08); + } +`; + +const GaugeFill = styled.div` + height: 100%; + border-radius: 9999px; + transition: width 0.6s cubic-bezier(0.16, 1, 0.3, 1); +`; + +const InfoCardsRow = styled.div` + display: flex; + gap: 10px; + margin-bottom: 10px; + flex-wrap: wrap; +`; + +const InfoPill = styled.div` + display: inline-flex; + align-items: center; + gap: 6px; + font-size: ${typographyRole.label.fontSize}; + color: ${meok[700]}; + background: #ffffff; + padding: 8px 14px; + border-radius: 10px; + border: none; + box-shadow: none; + + [data-theme='dark'] & { + background: ${surface.dark.app}; + color: ${meok[200]}; + } +`; + +const PillLabel = styled.span` + font-weight: 600; + color: ${meok[500]}; + + [data-theme='dark'] & { + color: ${meok[400]}; + } +`; + +const PillVal = styled.span` + font-weight: 700; + color: ${meok[900]}; + + [data-theme='dark'] & { + color: ${meok[100]}; + } +`; + +const AdviceText = styled.div` + display: flex; + align-items: flex-start; + gap: 8px; + font-size: ${typographyRole.label.fontSize}; + line-height: 1.5; + color: ${meok[500]}; + background: rgba(0, 0, 0, 0.02); + padding: 10px 14px; + border-radius: 10px; + border: none; + box-shadow: none; + + [data-theme='dark'] & { + background: rgba(255, 255, 255, 0.04); + color: ${meok[200]}; + } +`; diff --git a/src/features/hanok-archive/components/VillageCard.tsx b/src/features/hanok-archive/components/VillageCard.tsx index 094c4109..f4daa951 100644 --- a/src/features/hanok-archive/components/VillageCard.tsx +++ b/src/features/hanok-archive/components/VillageCard.tsx @@ -4,7 +4,7 @@ import React from 'react'; import styled from '@emotion/styled'; import { motion } from 'framer-motion'; import { transientProps } from '@/design-system/styled'; -import { meok, palette, fontSize } from '@/design-system/tokens'; +import { meok, palette, typographyRole } from '@/design-system/tokens'; import { HugeiconsIcon } from '@hugeicons/react'; import { HeadphonesIcon, MapPinIcon, ArrowUpRight01Icon } from '@hugeicons/core-free-icons'; import type { Village } from '@/features/hanok-archive/types'; @@ -243,7 +243,7 @@ const TypeBadge = styled.span` -webkit-backdrop-filter: blur(10px); border: 1px solid rgba(0, 0, 0, 0.06); color: ${meok[800]}; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 600; padding: 4px 10px; border-radius: 9999px; @@ -266,7 +266,7 @@ const DocentTag = styled.span` -webkit-backdrop-filter: blur(10px); border: 1px solid rgba(0, 0, 0, 0.06); color: ${meok[800]}; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 600; padding: 4px 10px; border-radius: 9999px; @@ -298,7 +298,7 @@ const TypographyArea = styled.div` const Name = styled.h3` font-family: var(--font-hanok); - font-size: 1.02rem; + font-size: ${typographyRole.cardTitle.fontSize}; font-weight: 700; color: ${meok[900]}; margin: 0; @@ -327,7 +327,7 @@ const MetaRow = styled.div` `; const RegionText = styled.p` - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 500; color: ${meok[500]}; margin: 0; diff --git a/src/features/hanok-archive/components/VillageDetailModal.styles.ts b/src/features/hanok-archive/components/VillageDetailModal.styles.ts index 20a928fd..bc8257fd 100644 --- a/src/features/hanok-archive/components/VillageDetailModal.styles.ts +++ b/src/features/hanok-archive/components/VillageDetailModal.styles.ts @@ -1,6 +1,6 @@ -import styled from '@emotion/styled'; +import styled from '@emotion/styled'; import { motion } from 'framer-motion'; -import { meok, palette, surface, fluidHeading, fontSize } from '@/design-system/tokens'; +import { meok, palette, surface, typographyRole } from '@/design-system/tokens'; export const Overlay = styled(motion.div)` position: fixed; @@ -113,7 +113,7 @@ export const HeroContent = styled.div` export const HeroRegion = styled.span` display: inline-block; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 500; color: ${palette.hwanggeum[200]}; margin-bottom: 6px; @@ -123,7 +123,7 @@ export const HeroRegion = styled.span` export const HeroTitle = styled.h2` font-family: var(--font-hanok); - font-size: ${fluidHeading.feature}; + font-size: ${typographyRole.sectionTitle.fontSize}; font-weight: 400; margin: 0; line-height: 1.25; @@ -159,7 +159,7 @@ export const MetaRow = styled.div` export const TypeBadge = styled.span` background: #f0f0ee; color: ${meok[800]}; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 600; padding: 4px 12px; border-radius: 9999px; @@ -172,7 +172,7 @@ export const TypeBadge = styled.span` `; export const AddrText = styled.span` - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; color: ${meok[500]}; [data-theme='dark'] & { @@ -197,7 +197,7 @@ export const NoteHeader = styled.div` display: flex; align-items: center; justify-content: space-between; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 600; color: ${meok[900]}; margin-bottom: 12px; @@ -214,7 +214,7 @@ export const HeaderBadge = styled.div` `; export const SourceTag = styled.span` - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; font-weight: 500; color: ${meok[700]}; background: ${meok[200]}; @@ -237,7 +237,7 @@ export const StoryContainer = styled.div<{ $isExpanded: boolean }>` `; export const StoryParagraph = styled.p` - font-size: 15px; + font-size: ${typographyRole.body.fontSize}; font-weight: 450; color: ${meok[800]}; line-height: 1.82; @@ -257,7 +257,7 @@ export const StoryParagraph = styled.p` export const ExpandBtn = styled.button` background: transparent; color: ${palette.juhong[600]}; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 600; cursor: pointer; padding: 6px 0 0; @@ -277,7 +277,7 @@ export const ExpandBtn = styled.button` `; export const SectionTitle = styled.h3` - font-size: ${fontSize.base}; + font-size: ${typographyRole.body.fontSize}; font-weight: 400; color: ${meok[900]}; margin: 24px 0 12px; @@ -342,7 +342,7 @@ export const InfoContentBox = styled.div` `; export const InfoLabel = styled.div` - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 500; color: ${meok[500]}; margin-bottom: 3px; @@ -353,7 +353,7 @@ export const InfoLabel = styled.div` `; export const InfoVal = styled.div` - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 400; color: ${meok[700]}; line-height: 1.5; @@ -396,7 +396,7 @@ export const RepeatItemCard = styled.div` `; export const RepeatTitleText = styled.div` - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 600; color: ${meok[700]}; margin-bottom: 4px; @@ -407,7 +407,7 @@ export const RepeatTitleText = styled.div` `; export const RepeatContentText = styled.div` - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; color: ${meok[700]}; line-height: 1.6; white-space: pre-wrap; @@ -505,7 +505,7 @@ export const MapBtn = styled.a` background: ${meok[900]}; color: #ffffff; border-radius: 14px; - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; font-weight: 500; text-decoration: none; border: none; @@ -539,7 +539,7 @@ export const BookingModalBtn = styled.a` background: ${palette.juhong[500]}; color: #ffffff; border-radius: 14px; - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; font-weight: 600; text-decoration: none; border: none; @@ -594,7 +594,7 @@ export const BookmarkActionBtn = styled.button<{ $bookmarked: boolean }>` height: 48px; padding: 0 18px; border-radius: 14px; - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; font-weight: 500; cursor: pointer; border: none; @@ -629,7 +629,7 @@ export const HeroZoomBadge = styled.button` border: none; box-shadow: none; color: #ffffff; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 500; padding: 6px 14px; border-radius: 9999px; @@ -736,7 +736,7 @@ export const LightboxFooter = styled.div` align-items: center; gap: 12px; color: #ffffff; - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; `; export const LightboxCounter = styled.span` @@ -744,7 +744,7 @@ export const LightboxCounter = styled.span` backdrop-filter: blur(8px); padding: 4px 14px; border-radius: 9999px; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 500; letter-spacing: 0.05em; `; @@ -762,7 +762,7 @@ export const InsightBadge = styled.span<{ $color: string }>` gap: 4px; padding: 4px 10px; border-radius: 9999px; - font-size: 11.5px; + font-size: ${typographyRole.caption.fontSize}; font-weight: 600; color: ${({ $color }) => $color}; background: ${({ $color }) => $color}16; @@ -824,7 +824,7 @@ export const MapPreviewLabel = styled.div` display: flex; align-items: center; gap: 4px; - font-size: 11px; + font-size: ${typographyRole.caption.fontSize}; font-weight: 600; color: ${palette.juhong[600]}; margin-bottom: 3px; @@ -837,7 +837,7 @@ export const MapPreviewLabel = styled.div` `; export const MapPreviewName = styled.div` - font-size: 14px; + font-size: ${typographyRole.bodySm.fontSize}; font-weight: 600; color: ${meok[900]}; letter-spacing: -0.02em; @@ -852,7 +852,7 @@ export const MapPreviewName = styled.div` `; export const MapPreviewAddr = styled.div` - font-size: 11.5px; + font-size: ${typographyRole.caption.fontSize}; color: ${meok[600]}; white-space: nowrap; overflow: hidden; @@ -868,7 +868,7 @@ export const MapPreviewAction = styled.div` display: flex; align-items: center; gap: 3px; - font-size: 12px; + font-size: ${typographyRole.label.fontSize}; font-weight: 600; color: ${palette.juhong[600]}; white-space: nowrap; @@ -895,7 +895,7 @@ export const ExpguideLabel = styled.div` display: flex; align-items: center; gap: 5px; - font-size: 11.5px; + font-size: ${typographyRole.caption.fontSize}; font-weight: 600; color: ${meok[600]}; margin-bottom: 8px; @@ -908,7 +908,7 @@ export const ExpguideLabel = styled.div` export const ExpguideText = styled.p` margin: 0; - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; line-height: 1.7; color: ${meok[800]}; white-space: pre-line; @@ -947,7 +947,7 @@ export const QuickInfoTitle = styled.h3` display: flex; align-items: center; gap: 7px; - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; font-weight: 600; color: ${meok[900]}; margin: 0; @@ -1005,7 +1005,7 @@ export const QuickInfoBody = styled.div` `; export const QuickInfoItemLabel = styled.div` - font-size: 11px; + font-size: ${typographyRole.caption.fontSize}; font-weight: 500; color: ${meok[500]}; margin-bottom: 2px; @@ -1017,7 +1017,7 @@ export const QuickInfoItemLabel = styled.div` `; export const QuickInfoItemValue = styled.div` - font-size: 13px; + font-size: ${typographyRole.caption.fontSize}; font-weight: 500; color: ${meok[900]}; line-height: 1.45; @@ -1047,7 +1047,7 @@ export const CopyBtn = styled.button` margin-left: 6px; padding: 2px 7px; border-radius: 6px; - font-size: 11px; + font-size: ${typographyRole.caption.fontSize}; font-weight: 500; background: ${meok[200]}; color: ${meok[800]}; diff --git a/src/features/hanok-archive/sections/HanokGrid.tsx b/src/features/hanok-archive/sections/HanokGrid.tsx index 56bfaaf4..63beb960 100644 --- a/src/features/hanok-archive/sections/HanokGrid.tsx +++ b/src/features/hanok-archive/sections/HanokGrid.tsx @@ -2,7 +2,7 @@ import React, { useState, useMemo, useEffect } from 'react'; import styled from '@emotion/styled'; -import { meok, surface , fontSize } from '@/design-system/tokens'; +import { meok, surface, typographyRole, spacingLayout, mq } from '@/design-system/tokens'; import SectionHeader from '@/features/hanok-archive/components/SectionHeader'; import FilterBar, { type VillageTypeFilter } from '@/features/hanok-archive/components/FilterBar'; import VillageCard from '@/features/hanok-archive/components/VillageCard'; @@ -22,23 +22,19 @@ const Section = styled.section``; const Grid = styled(motion.div)` display: grid; grid-template-columns: repeat(1, 1fr); - gap: 20px; + gap: ${spacingLayout.cardGap}; - @media (min-width: 400px) { + ${mq.tablet} { grid-template-columns: repeat(2, 1fr); - gap: 14px; } - @media (min-width: 800px) { + ${mq.laptop} { grid-template-columns: repeat(3, 1fr); - gap: 20px; } - @media (min-width: 1080px) { + ${mq.desktop} { grid-template-columns: repeat(4, 1fr); - gap: 24px; } - `; const containerVariants = { @@ -66,7 +62,7 @@ const EmptyState = styled.div` justify-content: center; gap: 14px; color: ${meok[500]}; - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; text-align: center; word-break: keep-all; @@ -92,7 +88,7 @@ const ResetAll = styled.button` border-radius: 9999px; background: #ffffff; font-family: inherit; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 500; color: ${meok[700]}; cursor: pointer; diff --git a/src/features/hanok-archive/sections/HanokHero.tsx b/src/features/hanok-archive/sections/HanokHero.tsx index 218c5a16..b696ea7a 100644 --- a/src/features/hanok-archive/sections/HanokHero.tsx +++ b/src/features/hanok-archive/sections/HanokHero.tsx @@ -4,7 +4,7 @@ import React, { useState } from 'react'; import styled from '@emotion/styled'; import { motion, AnimatePresence } from 'framer-motion'; import { transientProps } from '@/design-system/styled'; -import { meok, lightPalette, fluidHeading , fontSize } from '@/design-system/tokens'; +import { meok, lightPalette, typographyRole } from '@/design-system/tokens'; import { HugeiconsIcon } from '@hugeicons/react' import { SparklesIcon, ArrowRight01Icon } from '@hugeicons/core-free-icons' import type { Village } from '@/features/hanok-archive/types'; @@ -60,7 +60,7 @@ const EyebrowBadge = styled.div` -webkit-backdrop-filter: blur(12px); padding: 6px 14px; border-radius: 9999px; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 400; color: #ffffff; @@ -74,7 +74,7 @@ const EyebrowBadge = styled.div` const Title = styled.h2` font-family: var(--font-traditional-title); - font-size: ${fluidHeading.hero}; + font-size: ${typographyRole.sectionTitle.fontSize}; font-weight: 700; color: #ffffff; margin: 0 0 12px; @@ -84,7 +84,7 @@ const Title = styled.h2` const Description = styled.p` font-family: var(--font-traditional-body); - font-size: 15px; + font-size: ${typographyRole.body.fontSize}; color: rgba(255, 255, 255, 0.9); line-height: 1.7; max-width: 680px; @@ -126,7 +126,7 @@ const DetailBtn = styled.button` gap: 8px; background: #ffffff; color: ${meok[900]}; - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; font-weight: 500; padding: 10px 22px; border-radius: 9999px; diff --git a/src/features/hanok-archive/sections/HanokInteractiveMapFrame.tsx b/src/features/hanok-archive/sections/HanokInteractiveMapFrame.tsx index cc10ac02..df2356b7 100644 --- a/src/features/hanok-archive/sections/HanokInteractiveMapFrame.tsx +++ b/src/features/hanok-archive/sections/HanokInteractiveMapFrame.tsx @@ -6,7 +6,7 @@ import styled from '@emotion/styled'; import { motion, AnimatePresence } from 'framer-motion'; import { HugeiconsIcon } from '@hugeicons/react' import { AlertCircleIcon, BookOpen01Icon, ChevronLeftIcon, ChevronRightIcon, MapIcon, MapPinIcon } from '@hugeicons/core-free-icons' -import { lightPalette, meok, surface , fontSize } from '@/design-system/tokens'; +import { lightPalette, meok, surface, typographyRole } from '@/design-system/tokens'; import type { Village } from '@/features/hanok-archive/types'; import { filterLabel } from '@/features/hanok-archive/filterLabels'; import { createKakaoResourceScope, type KakaoResourceScope } from './kakaoMapResources'; @@ -113,7 +113,7 @@ const MapCanvas = styled.div` -webkit-backdrop-filter: blur(8px); border: 1px solid ${JUHONG_BORDER}; border-radius: 10px; - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; font-weight: 500; letter-spacing: -0.01em; color: ${meok[900]}; @@ -137,7 +137,7 @@ const MapLoadingState = styled.div` gap: 10px; background: #ffffff; color: ${JUHONG_PRIMARY}; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 500; z-index: 10; text-align: center; @@ -149,7 +149,7 @@ const MapLoadingState = styled.div` `; const ErrorSubtext = styled.p` - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; color: ${meok[700]}; max-width: 340px; line-height: 1.45; @@ -198,7 +198,7 @@ const RegionChip = styled.button<{ $active: boolean }>` border: none; border-radius: 9999px; padding: 5px 11px; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: ${({ $active }) => ($active ? 500 : 400)}; white-space: nowrap; cursor: pointer; @@ -263,7 +263,7 @@ const PanelHeaderRow = styled.div` `; const PanelSubHeader = styled.div` - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; font-weight: 500; letter-spacing: 0.03em; color: ${JUHONG_PRIMARY}; @@ -294,7 +294,7 @@ const CollapseBtn = styled.button` const PanelTitle = styled.h3` font-family: var(--font-hanok); - font-size: ${fontSize.base}; + font-size: ${typographyRole.body.fontSize}; font-weight: 500; color: ${meok[900]}; margin: 0 0 4px; @@ -307,7 +307,7 @@ const PanelTitle = styled.h3` const PanelCountBadge = styled.span` display: inline-block; - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; font-weight: 500; color: ${JUHONG_PRIMARY}; background: ${JUHONG_LIGHT}; @@ -319,7 +319,7 @@ const PanelCountBadge = styled.span` `; const PanelDesc = styled.p` - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; color: ${meok[700]}; line-height: 1.45; margin: 0 0 10px; @@ -390,7 +390,7 @@ const MiniInfo = styled.div` `; const MiniTitle = styled.h4` - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 500; color: ${meok[900]}; margin: 0 0 2px; @@ -403,12 +403,12 @@ const MiniTitle = styled.h4` } @media (max-width: 900px) { - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; } `; const MiniMeta = styled.div` - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; font-weight: 400; color: ${meok[500]}; @@ -433,7 +433,7 @@ const CollapsedPillBtn = styled(motion.button)` align-items: center; gap: 6px; color: ${meok[900]}; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 500; cursor: pointer; box-shadow: none; diff --git a/src/features/hanok-archive/sections/HanokManifestoCta.tsx b/src/features/hanok-archive/sections/HanokManifestoCta.tsx index 465eaf75..2e562904 100644 --- a/src/features/hanok-archive/sections/HanokManifestoCta.tsx +++ b/src/features/hanok-archive/sections/HanokManifestoCta.tsx @@ -4,7 +4,7 @@ import React, { useState, useEffect, useMemo } from 'react'; import styled from '@emotion/styled'; import Link from 'next/link'; import { motion, useReducedMotion } from 'framer-motion'; -import { meok, palette, surface, fontSize } from '@/design-system/tokens'; +import { meok, palette, surface, typographyRole } from '@/design-system/tokens'; import { HugeiconsIcon } from '@hugeicons/react'; import { ArrowRight01Icon } from '@hugeicons/core-free-icons'; @@ -54,6 +54,7 @@ const Container = styled.div` const ManifestoParagraph = styled(motion.h2)` font-family: 'Dohyun', 'Pretendard', var(--font-hanok), -apple-system, BlinkMacSystemFont, system-ui, Roboto, sans-serif; + /* ponytail: 예외 — Dohyun 폰트 매니페스토 시 감성 카피, display보다 절제된 극적 크기 의도 */ font-size: clamp(20px, 3.2vw, 38px); font-weight: 400; line-height: 1.8; @@ -104,7 +105,7 @@ const CtaButton = styled(Link, { background: ${({ $primary }) => $primary ? palette.juhong[500] : '#f5f5f4'}; color: ${({ $primary }) => ($primary ? '#ffffff' : meok[900])}; - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; font-weight: ${({ $primary }) => ($primary ? 700 : 500)}; padding: 14px 28px; border-radius: 9999px; diff --git a/src/features/hanok-archive/sections/HanokMap.tsx b/src/features/hanok-archive/sections/HanokMap.tsx index 2c1b7839..62fc1897 100644 --- a/src/features/hanok-archive/sections/HanokMap.tsx +++ b/src/features/hanok-archive/sections/HanokMap.tsx @@ -3,7 +3,7 @@ import React from 'react'; import styled from '@emotion/styled'; import dynamic from 'next/dynamic'; -import { meok, surface , fontSize } from '@/design-system/tokens'; +import { meok, surface, typographyRole } from '@/design-system/tokens'; import SectionHeader from '@/features/hanok-archive/components/SectionHeader'; import type { Village } from '@/features/hanok-archive/types'; import { useViewportActivation } from '@/shared/hooks/useViewportActivation'; @@ -28,7 +28,7 @@ const MapLoadingState = styled.div` display: grid; place-items: center; color: ${meok[500]}; - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; [data-theme='dark'] & { background: ${surface.dark.card}; diff --git a/src/features/hanok-archive/sections/HanokStayAccordion.tsx b/src/features/hanok-archive/sections/HanokStayAccordion.tsx index a3475c9f..e539897a 100644 --- a/src/features/hanok-archive/sections/HanokStayAccordion.tsx +++ b/src/features/hanok-archive/sections/HanokStayAccordion.tsx @@ -4,7 +4,7 @@ import React, { useState, useMemo, useEffect } from 'react'; import styled from '@emotion/styled'; import { motion, AnimatePresence, useReducedMotion } from 'framer-motion'; import { transientProps } from '@/design-system/styled'; -import { meok, palette, surface, fluidHeading , fontSize } from '@/design-system/tokens'; +import { meok, palette, surface, typographyRole, mq } from '@/design-system/tokens'; import SectionHeader from '@/features/hanok-archive/components/SectionHeader'; import { STAY_TYPE } from '@/features/hanok-archive/types'; import type { Village } from '@/features/hanok-archive/types'; @@ -28,7 +28,7 @@ const FilterToolbar = styled.div` gap: 16px; margin-bottom: 24px; - @media (max-width: 768px) { + ${mq.mobile} { grid-template-columns: minmax(0, 1fr); gap: 6px; } @@ -51,7 +51,7 @@ const ResultCount = styled.span` align-items: center; justify-content: flex-end; color: ${meok[500]}; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 500; white-space: nowrap; @@ -59,7 +59,7 @@ const ResultCount = styled.span` color: ${meok[400]}; } - @media (max-width: 768px) { + ${mq.mobile} { justify-self: end; } `; @@ -71,7 +71,7 @@ const RegionFilterChip = styled.button<{ $active: boolean; $empty?: boolean }>` background: ${({ $active }) => $active ? palette.juhong[500] : '#ffffff'}; color: ${({ $active }) => ($active ? '#ffffff' : meok[900])}; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: ${({ $active }) => ($active ? 500 : 400)}; padding: 8px 18px; border-radius: 9999px; @@ -178,7 +178,7 @@ const PageCounter = styled.span` min-width: 48px; text-align: center; color: ${meok[500]}; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-variant-numeric: tabular-nums; font-weight: 500; @@ -347,7 +347,7 @@ const TagRow = styled.div` `; const StayTag = styled.span` - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 500; color: #ffffff; background: rgba(0, 0, 0, 0.45); @@ -360,7 +360,7 @@ const StayTag = styled.span` const StayTitle = styled.h3` font-family: var(--font-hanok); - font-size: ${fluidHeading.card}; + font-size: ${typographyRole.cardTitle.fontSize}; font-weight: 500; letter-spacing: -0.02em; margin: 0; @@ -378,7 +378,7 @@ const StayAddress = styled.div` display: flex; align-items: center; gap: 5px; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 400; color: rgba(255, 255, 255, 0.88); line-height: 1.4; @@ -453,7 +453,7 @@ const ActionGroup = styled.div` const DirectBookingBtn = styled.a` background: ${palette.juhong[500]}; color: #ffffff; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 500; padding: 8px 15px; border-radius: 9999px; @@ -493,7 +493,7 @@ const DetailActionBtn = styled.button` -webkit-backdrop-filter: blur(10px); border: 1px solid rgba(255, 255, 255, 0.12); color: #ffffff; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 500; padding: 9px 15px; border-radius: 9999px; @@ -545,7 +545,7 @@ const EmptyState = styled.div` const EmptyHeadline = styled.p` margin: 0; - font-size: ${fluidHeading.label}; + font-size: ${typographyRole.body.fontSize}; font-weight: 300; letter-spacing: -0.02em; color: ${meok[900]}; @@ -558,7 +558,7 @@ const EmptyHeadline = styled.p` const EmptyHint = styled.p` margin: 0; - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; font-weight: 400; line-height: 1.7; color: ${meok[500]}; @@ -576,7 +576,7 @@ const EmptyAction = styled.button` background: ${meok[900]}; color: #ffffff; font-family: inherit; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 700; cursor: pointer; transition: opacity 0.18s ease, transform 0.18s ease; @@ -764,11 +764,11 @@ export default function HanokStayAccordion({ const [isMobile, setIsMobile] = useState(false); useEffect(() => { - const mq = window.matchMedia('(max-width: 767px)'); - setIsMobile(mq.matches); + const mediaQuery = window.matchMedia('(max-width: 767px)'); + setIsMobile(mediaQuery.matches); const handler = (e: MediaQueryListEvent) => setIsMobile(e.matches); - mq.addEventListener('change', handler); - return () => mq.removeEventListener('change', handler); + mediaQuery.addEventListener('change', handler); + return () => mediaQuery.removeEventListener('change', handler); }, []); const allStays = useMemo(() => { diff --git a/src/features/hanok-archive/structure/HanokAssemblyModal.tsx b/src/features/hanok-archive/structure/HanokAssemblyModal.tsx index c2dff2d2..96ceb6b2 100644 --- a/src/features/hanok-archive/structure/HanokAssemblyModal.tsx +++ b/src/features/hanok-archive/structure/HanokAssemblyModal.tsx @@ -1,273 +1,273 @@ -'use client'; - - - - - - - - - - - - - - - - - - - -import { useLayoutEffect, useRef, useState } from 'react'; -import styled from '@emotion/styled'; - -import { meok, lightPalette, surface , fontSize } from '@/design-system/tokens'; -import { ASSEMBLY_RANGE } from './constants'; -import { STAGES } from './stages'; -import { clamp01 } from './motion'; -import StructureModal from './StructureModal'; -import StructureCanvas from './StructureCanvas'; -import HanokAssemblyPanel, { STAGE_WINDOWS, activeStageOf } from './HanokAssemblyPanel'; - - - - - - - - -const STAGE_EPSILON = 0.0005; - -const endOfStage = (i: number) => - i === STAGES.length - 1 ? 1 : STAGE_WINDOWS[i][1] - STAGE_EPSILON; - - - - - - -const ASSEMBLY_SCROLL_LENGTH = '600%'; - -const Body = styled.div` - position: relative; - width: 100%; - height: 100%; - overflow: hidden; - background: ${surface.light.base}; - - --sim-fade: rgba(255, 255, 255, 0.94); - --sim-pill: rgba(255, 255, 255, 0.9); - --sim-pill-border: rgba(25, 31, 40, 0.12); - --sim-ink-weak: ${meok[500]}; - - [data-theme='dark'] & { - background: ${surface.dark.surface}; - - --sim-fade: rgba(11, 18, 32, 0.94); - --sim-pill: rgba(33, 39, 52, 0.9); - --sim-pill-border: rgba(255, 255, 255, 0.12); - --sim-ink-weak: ${meok[400]}; - } -`; - -const Scroller = styled.div` - position: absolute; - inset: 0; - overflow-y: auto; - - overscroll-behavior: contain; - scrollbar-width: none; - - &::-webkit-scrollbar { - display: none; - } -`; - -const Stage = styled.div` - position: sticky; - top: 0; - - height: 100%; - overflow: hidden; -`; - -const Spacer = styled.div` - height: ${ASSEMBLY_SCROLL_LENGTH}; - pointer-events: none; -`; - - - - - - -const Controls = styled.div` - position: absolute; - left: 0; - right: 0; - bottom: 0; - z-index: 10; - display: flex; - flex-wrap: nowrap; - align-items: center; - justify-content: flex-start; - gap: 8px; - padding: 14px 18px 18px; - overflow-x: auto; - overscroll-behavior-x: contain; - scrollbar-width: none; - background: linear-gradient(to top, var(--sim-fade), rgba(255, 255, 255, 0)); - - &::-webkit-scrollbar { - display: none; - } - - - @media (min-width: 1024px) { - justify-content: center; - } - - > * { - flex: 0 0 auto; - } -`; - -const StepButton = styled.button<{ $active: boolean; $done: boolean }>` - display: inline-flex; - align-items: center; - gap: 6px; - height: 32px; - padding: 0 12px; - - border: 1px solid - ${({ $active, $done }) => - $active ? 'transparent' : $done ? lightPalette.juhong[200] : 'var(--sim-pill-border)'}; - border-radius: 9999px; - background: ${({ $active }) => ($active ? meok[900] : 'var(--sim-pill)')}; - color: ${({ $active, $done }) => - $active ? '#ffffff' : $done ? lightPalette.juhong[700] : 'var(--sim-ink-weak)'}; - font-family: inherit; - font-size: ${fontSize.xs}; - font-weight: 500; - letter-spacing: 0.04em; - white-space: nowrap; - cursor: pointer; - - &:hover { - border-color: ${({ $active }) => ($active ? 'transparent' : lightPalette.juhong[400])}; - color: ${({ $active }) => ($active ? '#ffffff' : lightPalette.juhong[700])}; - } - - small { - font-size: ${fontSize.micro}; - font-weight: 500; - opacity: 0.72; - } -`; - - -const ScrollHint = styled.p` - position: absolute; - left: 0; - right: 0; - bottom: 64px; - z-index: 10; - margin: 0; - text-align: center; - font-size: ${fontSize.xs}; - font-weight: 400; - color: var(--sim-ink-weak); - pointer-events: none; -`; - -interface HanokAssemblyModalProps { - onClose: () => void; - - initialStage?: number; -} - -export default function HanokAssemblyModal({ onClose, initialStage }: HanokAssemblyModalProps) { - const scrollerRef = useRef(null); - const [local, setLocal] = useState(() => - initialStage === undefined ? 0 : endOfStage(initialStage), - ); - - const readProgress = () => { - const el = scrollerRef.current; - if (!el) return; - - const travel = el.scrollHeight - el.clientHeight; - setLocal(travel > 0 ? clamp01(el.scrollTop / travel) : 0); - }; - - - const scrollToStage = (i: number) => { - const el = scrollerRef.current; - if (!el) return; - - el.scrollTo({ - top: endOfStage(i) * (el.scrollHeight - el.clientHeight), - behavior: 'smooth', - }); - }; - - - - - - - - - - useLayoutEffect(() => { - if (initialStage === undefined) return undefined; - - const frame = requestAnimationFrame(() => { - const el = scrollerRef.current; - if (!el) return; - - const travel = el.scrollHeight - el.clientHeight; - if (travel > 0) el.scrollTop = endOfStage(initialStage) * travel; - }); - - return () => cancelAnimationFrame(frame); - }, [initialStage]); - - const activeIndex = Math.max(0, activeStageOf(local)); - - return ( - - - - - - - - - {local < 0.01 && 스크롤을 내려 한옥을 세워보세요} - - - {STAGES.map((stage, i) => ( - = endOfStage(i)} - onClick={() => scrollToStage(i)} - aria-current={i === activeIndex ? 'step' : undefined} - > - {String(stage.step).padStart(2, '0')} - {stage.nameKo} - - ))} - - - - - - - ); -} +'use client'; + + + + + + + + + + + + + + + + + + + +import { useLayoutEffect, useRef, useState } from 'react'; +import styled from '@emotion/styled'; + +import { meok, lightPalette, surface, typographyRole } from '@/design-system/tokens'; +import { ASSEMBLY_RANGE } from './constants'; +import { STAGES } from './stages'; +import { clamp01 } from './motion'; +import StructureModal from './StructureModal'; +import StructureCanvas from './StructureCanvas'; +import HanokAssemblyPanel, { STAGE_WINDOWS, activeStageOf } from './HanokAssemblyPanel'; + + + + + + + + +const STAGE_EPSILON = 0.0005; + +const endOfStage = (i: number) => + i === STAGES.length - 1 ? 1 : STAGE_WINDOWS[i][1] - STAGE_EPSILON; + + + + + + +const ASSEMBLY_SCROLL_LENGTH = '600%'; + +const Body = styled.div` + position: relative; + width: 100%; + height: 100%; + overflow: hidden; + background: ${surface.light.base}; + + --sim-fade: rgba(255, 255, 255, 0.94); + --sim-pill: rgba(255, 255, 255, 0.9); + --sim-pill-border: rgba(25, 31, 40, 0.12); + --sim-ink-weak: ${meok[500]}; + + [data-theme='dark'] & { + background: ${surface.dark.surface}; + + --sim-fade: rgba(11, 18, 32, 0.94); + --sim-pill: rgba(33, 39, 52, 0.9); + --sim-pill-border: rgba(255, 255, 255, 0.12); + --sim-ink-weak: ${meok[400]}; + } +`; + +const Scroller = styled.div` + position: absolute; + inset: 0; + overflow-y: auto; + + overscroll-behavior: contain; + scrollbar-width: none; + + &::-webkit-scrollbar { + display: none; + } +`; + +const Stage = styled.div` + position: sticky; + top: 0; + + height: 100%; + overflow: hidden; +`; + +const Spacer = styled.div` + height: ${ASSEMBLY_SCROLL_LENGTH}; + pointer-events: none; +`; + + + + + + +const Controls = styled.div` + position: absolute; + left: 0; + right: 0; + bottom: 0; + z-index: 10; + display: flex; + flex-wrap: nowrap; + align-items: center; + justify-content: flex-start; + gap: 8px; + padding: 14px 18px 18px; + overflow-x: auto; + overscroll-behavior-x: contain; + scrollbar-width: none; + background: linear-gradient(to top, var(--sim-fade), rgba(255, 255, 255, 0)); + + &::-webkit-scrollbar { + display: none; + } + + + @media (min-width: 1024px) { + justify-content: center; + } + + > * { + flex: 0 0 auto; + } +`; + +const StepButton = styled.button<{ $active: boolean; $done: boolean }>` + display: inline-flex; + align-items: center; + gap: 6px; + height: 32px; + padding: 0 12px; + + border: 1px solid + ${({ $active, $done }) => + $active ? 'transparent' : $done ? lightPalette.juhong[200] : 'var(--sim-pill-border)'}; + border-radius: 9999px; + background: ${({ $active }) => ($active ? meok[900] : 'var(--sim-pill)')}; + color: ${({ $active, $done }) => + $active ? '#ffffff' : $done ? lightPalette.juhong[700] : 'var(--sim-ink-weak)'}; + font-family: inherit; + font-size: ${typographyRole.label.fontSize}; + font-weight: 500; + letter-spacing: 0.04em; + white-space: nowrap; + cursor: pointer; + + &:hover { + border-color: ${({ $active }) => ($active ? 'transparent' : lightPalette.juhong[400])}; + color: ${({ $active }) => ($active ? '#ffffff' : lightPalette.juhong[700])}; + } + + small { + font-size: ${typographyRole.caption.fontSize}; + font-weight: 500; + opacity: 0.72; + } +`; + + +const ScrollHint = styled.p` + position: absolute; + left: 0; + right: 0; + bottom: 64px; + z-index: 10; + margin: 0; + text-align: center; + font-size: ${typographyRole.label.fontSize}; + font-weight: 400; + color: var(--sim-ink-weak); + pointer-events: none; +`; + +interface HanokAssemblyModalProps { + onClose: () => void; + + initialStage?: number; +} + +export default function HanokAssemblyModal({ onClose, initialStage }: HanokAssemblyModalProps) { + const scrollerRef = useRef(null); + const [local, setLocal] = useState(() => + initialStage === undefined ? 0 : endOfStage(initialStage), + ); + + const readProgress = () => { + const el = scrollerRef.current; + if (!el) return; + + const travel = el.scrollHeight - el.clientHeight; + setLocal(travel > 0 ? clamp01(el.scrollTop / travel) : 0); + }; + + + const scrollToStage = (i: number) => { + const el = scrollerRef.current; + if (!el) return; + + el.scrollTo({ + top: endOfStage(i) * (el.scrollHeight - el.clientHeight), + behavior: 'smooth', + }); + }; + + + + + + + + + + useLayoutEffect(() => { + if (initialStage === undefined) return undefined; + + const frame = requestAnimationFrame(() => { + const el = scrollerRef.current; + if (!el) return; + + const travel = el.scrollHeight - el.clientHeight; + if (travel > 0) el.scrollTop = endOfStage(initialStage) * travel; + }); + + return () => cancelAnimationFrame(frame); + }, [initialStage]); + + const activeIndex = Math.max(0, activeStageOf(local)); + + return ( + + + + + + + + + {local < 0.01 && 스크롤을 내려 한옥을 세워보세요} + + + {STAGES.map((stage, i) => ( + = endOfStage(i)} + onClick={() => scrollToStage(i)} + aria-current={i === activeIndex ? 'step' : undefined} + > + {String(stage.step).padStart(2, '0')} + {stage.nameKo} + + ))} + + + + + + + ); +} diff --git a/src/features/hanok-archive/structure/HanokAssemblyPanel.jsx b/src/features/hanok-archive/structure/HanokAssemblyPanel.jsx index db8ed4e6..de17e9be 100644 --- a/src/features/hanok-archive/structure/HanokAssemblyPanel.jsx +++ b/src/features/hanok-archive/structure/HanokAssemblyPanel.jsx @@ -16,7 +16,7 @@ import { STAGES } from './stages'; import { MODEL_URL } from './constants'; useGLTF.preload(MODEL_URL); import { assemblyProgress, useSceneStore } from './sceneStore'; -import { meok, lightPalette } from '@/design-system/tokens'; +import { meok, lightPalette, typographyRole } from '@/design-system/tokens'; import { clamp01 } from './motion'; const FONT = 'var(--font-hanok)'; @@ -262,9 +262,9 @@ const TitleLine = styled.h2` margin: 0; display: flex; align-items: baseline; - font-size: clamp(36px, 4vw, 56px); + font-size: ${typographyRole.display.fontSize}; font-weight: 100; - letter-spacing: -0.015em; + letter-spacing: ${typographyRole.display.letterSpacing}; line-height: 1.05; background: ${titleGradient}; background-size: 200% 200%; @@ -310,9 +310,9 @@ const ResultWrapper = styled(motion.div)` const Result = styled(motion.p)` margin: 0; - font-size: clamp(26px, 3.0vw, 42px); + font-size: ${typographyRole.sectionTitle.fontSize}; font-weight: 400; - letter-spacing: -0.02em; + letter-spacing: ${typographyRole.sectionTitle.letterSpacing}; line-height: 1.35; background: ${titleGradient}; background-size: 200% 200%; diff --git a/src/features/hanok-archive/structure/HanokParts.tsx b/src/features/hanok-archive/structure/HanokParts.tsx index 22e16aa8..ba7f84b9 100644 --- a/src/features/hanok-archive/structure/HanokParts.tsx +++ b/src/features/hanok-archive/structure/HanokParts.tsx @@ -25,7 +25,7 @@ import React, { useState } from 'react'; import dynamic from 'next/dynamic'; import styled from '@emotion/styled'; -import { meok, palette, lightPalette, fluidHeading, fontSize } from '@/design-system/tokens'; +import { meok, palette, lightPalette, typographyRole } from '@/design-system/tokens'; import SectionHeader from '@/features/hanok-archive/components/SectionHeader'; import { STAGES } from './stages'; @@ -96,7 +96,7 @@ const Row = styled.button` `; const Step = styled.span` - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 500; font-variant-numeric: tabular-nums; letter-spacing: 0.04em; @@ -106,7 +106,7 @@ const Step = styled.span` const Name = styled.h3` font-family: var(--font-traditional-title); margin: 0; - font-size: ${fluidHeading.label}; + font-size: ${typographyRole.cardTitle.fontSize}; font-weight: 700; letter-spacing: -0.01em; line-height: 1.35; @@ -117,7 +117,7 @@ const Name = styled.h3` font-family: var(--font-traditional-body); display: block; margin-top: 3px; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 400; letter-spacing: 0.02em; color: ${meok[500]}; @@ -135,7 +135,7 @@ const Name = styled.h3` const Desc = styled.p` font-family: var(--font-traditional-body); margin: 0; - font-size: 14.5px; + font-size: ${typographyRole.bodySm.fontSize}; font-weight: 400; line-height: 1.8; color: ${meok[700]}; @@ -170,7 +170,7 @@ const Cue = styled.span` align-items: center; gap: 5px; margin-top: 10px; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 500; color: ${lightPalette.juhong[500]}; diff --git a/src/features/hanok-archive/structure/HanokStructureCards.tsx b/src/features/hanok-archive/structure/HanokStructureCards.tsx index 56e84f55..fdd5755a 100644 --- a/src/features/hanok-archive/structure/HanokStructureCards.tsx +++ b/src/features/hanok-archive/structure/HanokStructureCards.tsx @@ -17,7 +17,7 @@ import { motion } from 'framer-motion'; import { HugeiconsIcon } from '@hugeicons/react' import { ArrowUpRight01Icon, Layers01Icon, SunMediumIcon } from '@hugeicons/core-free-icons' -import { meok, palette, lightPalette, surface, fontSize, ringShadow } from '@/design-system/tokens'; +import { meok, palette, lightPalette, surface, ringShadow, typographyRole, spacingLayout } from '@/design-system/tokens'; import SectionHeader from '@/features/hanok-archive/components/SectionHeader'; const SolarShadowModal = dynamic(() => import('./SolarShadowModal'), { ssr: false }); @@ -33,9 +33,9 @@ const SectionWrapper = styled.section` const Grid = styled.div` display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: clamp(16px, 2.2vw, 28px); + gap: ${spacingLayout.cardGap}; - @media (max-width: 840px) { + @media (max-width: 767px) { grid-template-columns: 1fr; } `; @@ -79,25 +79,25 @@ const CardContainer = styled.button<{ $accentColor: string }>` `; const CardBody = styled.div` - padding: clamp(22px, 2.6vw, 30px) clamp(24px, 2.8vw, 36px) clamp(24px, 2.8vw, 36px); + padding: ${spacingLayout.cardPadding} clamp(24px, 2.8vw, 36px) ${spacingLayout.cardPadding}; `; const SectionLabel = styled.div<{ $color: string }>` display: flex; align-items: center; gap: 6px; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.eyebrow.fontSize}; font-weight: 700; - letter-spacing: 0.04em; + letter-spacing: ${typographyRole.eyebrow.letterSpacing}; color: ${({ $color }) => $color}; margin-bottom: 12px; `; const CardTitle = styled.h3` font-family: var(--font-hanok); - font-size: clamp(19px, 1.8vw, 23px); - font-weight: 700; - letter-spacing: -0.025em; + font-size: ${typographyRole.cardTitle.fontSize}; + font-weight: ${typographyRole.cardTitle.fontWeight}; + letter-spacing: ${typographyRole.cardTitle.letterSpacing}; line-height: 1.35; color: ${meok[900]}; margin: 0 0 8px; @@ -109,7 +109,7 @@ const CardTitle = styled.h3` `; const CardDesc = styled.p` - font-size: ${fontSize.sm}; + font-size: ${typographyRole.body.fontSize}; font-weight: 400; line-height: 1.65; color: ${meok[700]}; @@ -149,7 +149,7 @@ const PreviewBadge = styled.div` border-radius: 9999px; background: rgba(0, 0, 0, 0.55); color: #ffffff; - font-size: 14px; + font-size: ${typographyRole.bodySm.fontSize}; font-weight: 400; line-height: 1.35; letter-spacing: -0.01em; @@ -307,7 +307,7 @@ const MoreLink = styled.div<{ $color: string }>` align-items: center; gap: 4px; color: ${({ $color }) => $color}; - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; font-weight: 600; svg { diff --git a/src/features/hanok-archive/structure/SolarShadowPanel.jsx b/src/features/hanok-archive/structure/SolarShadowPanel.jsx index d1b02dca..86b3ad89 100644 --- a/src/features/hanok-archive/structure/SolarShadowPanel.jsx +++ b/src/features/hanok-archive/structure/SolarShadowPanel.jsx @@ -22,7 +22,7 @@ const FONT = 'var(--font-hanok)'; -import { meok, surface } from '@/design-system/tokens'; +import { meok, surface, typographyRole, mq } from '@/design-system/tokens'; @@ -274,7 +274,7 @@ const TermTag = styled.p` justify-content: center; gap: 8px; margin: 12px 0 0; - font-size: 12px; + font-size: ${typographyRole.label.fontSize}; font-weight: 500; letter-spacing: 0.1em; color: var(--sim-accent); @@ -304,10 +304,10 @@ const Headline = styled.h2` - font-size: clamp(21px, 2.5vw, 34px); + font-size: ${typographyRole.sectionTitle.fontSize}; font-weight: 400; - letter-spacing: -0.02em; + letter-spacing: ${typographyRole.sectionTitle.letterSpacing}; line-height: 1.25; word-break: keep-all; white-space: nowrap; @@ -317,8 +317,7 @@ const Headline = styled.h2` animation: ${fadeUp} 0.32s ease-out; - @media (max-width: 768px) { - font-size: clamp(19px, 2.9vw, 34px); + ${mq.mobile} { white-space: normal; text-wrap: balance; } @@ -346,7 +345,7 @@ const Stat = styled.div` const StatLabel = styled.dt` - font-size: clamp(11px, 1.1vw, 13px); + font-size: ${typographyRole.caption.fontSize}; font-weight: 500; letter-spacing: 0.06em; color: ${INK_WEAK}; @@ -356,9 +355,9 @@ const StatLabel = styled.dt` const StatValue = styled.dd` margin: 0; - font-size: clamp(17px, 1.9vw, 24px); + font-size: ${typographyRole.cardTitle.fontSize}; font-weight: 500; - letter-spacing: -0.02em; + letter-spacing: ${typographyRole.cardTitle.letterSpacing}; font-variant-numeric: tabular-nums; color: ${INK}; @@ -373,8 +372,8 @@ const StatValue = styled.dd` &[data-lead='true'] { - font-size: clamp(24px, 2.8vw, 34px); - letter-spacing: -0.03em; + font-size: ${typographyRole.numeric.fontSize}; + letter-spacing: ${typographyRole.numeric.letterSpacing}; } small { @@ -399,7 +398,7 @@ const StatValue = styled.dd` const Note = styled.p` margin: 7px auto 0; max-width: 420px; - font-size: 14px; + font-size: ${typographyRole.bodySm.fontSize}; line-height: 1.55; word-break: keep-all; color: ${INK_SUB}; @@ -407,9 +406,9 @@ const Note = styled.p` animation: ${fadeUp} 0.32s 0.06s ease-out both; - @media (max-width: 767px) { + ${mq.mobile} { margin-top: 6px; - font-size: 13px; + font-size: ${typographyRole.caption.fontSize}; line-height: 1.5; } @@ -442,7 +441,7 @@ const LocationButton = styled.button` border-radius: 9999px; background: transparent; font-family: inherit; - font-size: 12px; + font-size: ${typographyRole.label.fontSize}; line-height: 1.2; color: var(--sim-accent); @@ -523,7 +522,7 @@ const Hint = styled.p` box-shadow: 0 0 0 1px var(--sim-pill-border); background: var(--sim-pill); - font-size: 12px; + font-size: ${typographyRole.label.fontSize}; font-weight: 500; white-space: nowrap; color: ${INK_WEAK}; @@ -569,14 +568,14 @@ const HelpCard = styled.div` background: var(--sim-pill-strong); box-shadow: 0 12px 32px rgba(23, 30, 43, 0.13); color: ${INK_SUB}; - font-size: 12px; + font-size: ${typographyRole.label.fontSize}; line-height: 1.55; strong { display: block; margin-bottom: 4px; color: ${INK}; - font-size: 13px; + font-size: ${typographyRole.caption.fontSize}; } p { margin: 0; } @@ -795,7 +794,7 @@ const BackToToday = styled.button` box-shadow: 0 0 0 1px var(--sim-pill-border); background: var(--sim-pill-strong); font-family: inherit; - font-size: 13px; + font-size: ${typographyRole.caption.fontSize}; font-weight: 500; white-space: nowrap; color: ${ACCENT}; diff --git a/src/features/hanok-archive/structure/StructureModal.tsx b/src/features/hanok-archive/structure/StructureModal.tsx index 95874059..c06af2ac 100644 --- a/src/features/hanok-archive/structure/StructureModal.tsx +++ b/src/features/hanok-archive/structure/StructureModal.tsx @@ -13,7 +13,7 @@ import { HugeiconsIcon } from '@hugeicons/react' import { Cancel01Icon } from '@hugeicons/core-free-icons' import { createPortal } from 'react-dom'; -import { meok, surface , fontSize } from '@/design-system/tokens'; +import { meok, surface, typographyRole } from '@/design-system/tokens'; import { livelyModalEnter } from '@/shared/motion/modalMotion'; const Overlay = styled.div` @@ -62,7 +62,7 @@ const CloseButton = styled.button` color: ${meok[700]}; transition: background 0.2s ease-out, color 0.2s ease-out; font-family: inherit; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 500; cursor: pointer; diff --git a/src/features/map/MapPage.tsx b/src/features/map/MapPage.tsx index 4f938558..830268be 100644 --- a/src/features/map/MapPage.tsx +++ b/src/features/map/MapPage.tsx @@ -12,7 +12,7 @@ import { HugeiconsIcon } from '@hugeicons/react' import { ChevronLeftIcon, LocateFixedIcon } from '@hugeicons/core-free-icons' import { transientProps } from '@/design-system/styled'; -import { meok, surface, fontSize, palette } from '@/design-system/tokens'; +import { meok, surface, typographyRole, palette } from '@/design-system/tokens'; import { FLOATING_ENTER_DELAY_S, FLOATING_SPRING_TRANSITION, @@ -133,7 +133,7 @@ const FloatingHomeButton = styled.button` color: ${meok[700]}; font-family: 'Spoqa Han Sans Neo', sans-serif; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 500; letter-spacing: -0.02em; white-space: nowrap; diff --git a/src/features/map/components/CategoryChips.tsx b/src/features/map/components/CategoryChips.tsx index e4d4c5c5..c5bc659a 100644 --- a/src/features/map/components/CategoryChips.tsx +++ b/src/features/map/components/CategoryChips.tsx @@ -7,7 +7,7 @@ import { motion, AnimatePresence } from 'framer-motion'; import { HugeiconsIcon, type IconSvgElement } from '@hugeicons/react' import { Calendar01Icon, Coffee01Icon, FlameIcon, HeartIcon, LandmarkIcon, Leaf01Icon, Moon01Icon, ShoppingBag01Icon, SparklesIcon, UsersIcon, UtensilsIcon } from '@hugeicons/core-free-icons' import { transientProps } from '@/design-system/styled'; -import { meok, palette, fontFamily, fontSize, ringShadow } from '@/design-system/tokens'; +import { meok, palette, typographyRole, ringShadow } from '@/design-system/tokens'; import { useMapStore } from '@/features/map/hooks/useMapStore'; import type { MapInfoCategory, MapMode } from '@/features/map/types'; @@ -143,7 +143,7 @@ const Chip = styled.button<{ /* 폰트: 전체 카테고리 칩 통일 */ font-family: 'Spoqa Han Sans Neo', sans-serif; font-weight: ${({ $active }) => ($active ? 700 : 500)}; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; letter-spacing: -0.02em; /* 배경 & 폰트 컬러: @@ -198,7 +198,7 @@ const Chip = styled.button<{ @media (max-width: 1023px) { height: 32px; padding: 0 11px; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; gap: 4px; svg { diff --git a/src/features/map/components/InfoPlaceList.tsx b/src/features/map/components/InfoPlaceList.tsx index d2b44a37..e1fb2ec0 100644 --- a/src/features/map/components/InfoPlaceList.tsx +++ b/src/features/map/components/InfoPlaceList.tsx @@ -6,7 +6,7 @@ import { keyframes } from '@emotion/react'; import { isOnmaruApiError } from '@/lib/api/errors'; import { HugeiconsIcon } from '@hugeicons/react' import { AlertCircleIcon, ChevronLeftIcon, ListIcon, RotateCcwIcon } from '@hugeicons/core-free-icons' -import { meok, surface, fontSize } from '@/design-system/tokens'; +import { meok, surface, typographyRole } from '@/design-system/tokens'; import { useMapStore } from '@/features/map/hooks/useMapStore'; import { listInfoPlaces } from '@/features/map/services/infoMap.service'; import { mapInfoPlaceToItem } from '@/features/map/services/infoMarker.service'; @@ -51,7 +51,7 @@ const Breadcrumb = styled.div` align-items: center; gap: 4px; padding: 16px 14px 12px; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; color: ${meok[500]}; `; @@ -83,7 +83,7 @@ const Sentinel = styled.div` const LoadingMore = styled.div` text-align: center; padding: 16px; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; color: ${meok[400]}; `; @@ -96,7 +96,7 @@ const InlineError = styled.div` border-radius: 10px; background: #f5f5f4; color: ${meok[700]}; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; & > span { flex: 1; @@ -158,7 +158,7 @@ const EmptyBox = styled.div` text-align: center; gap: 8px; color: ${meok[500]}; - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; `; const RetryBtn = styled.button` @@ -171,7 +171,7 @@ const RetryBtn = styled.button` background: rgba(78, 89, 104, 0.08); color: ${meok[700]}; font-family: inherit; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 500; cursor: pointer; border: none; diff --git a/src/features/map/components/KakaoMap.tsx b/src/features/map/components/KakaoMap.tsx index 861c9923..58481581 100644 --- a/src/features/map/components/KakaoMap.tsx +++ b/src/features/map/components/KakaoMap.tsx @@ -8,7 +8,7 @@ import { Global, css } from '@emotion/react'; import { HugeiconsIcon } from '@hugeicons/react' import { Cancel01Icon, LocateFixedIcon, MinusSignIcon, Moon01Icon, PlusSignIcon, RotateCcwIcon, Sun01Icon } from '@hugeicons/core-free-icons' import { toast } from 'sonner'; -import { meok, lightPalette, surface, fontSize, palette } from '@/design-system/tokens'; +import { meok, surface, typographyRole, palette } from '@/design-system/tokens'; import { useOnmaruTheme } from '@/design-system/ThemeProvider'; import { KAKAO_SDK_SRC, snapshotFromMap, useKakaoMap } from '@/features/map/hooks/useKakaoMap'; import { useKakaoSdkLoad } from '@/features/map/hooks/useKakaoSdkLoad'; @@ -212,7 +212,7 @@ const MapRetryButton = styled.button` padding: 0 16px; color: ${meok[800]}; font-family: inherit; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 700; cursor: pointer; @@ -268,7 +268,7 @@ const Research = styled.button` color: ${meok[900]}; font-family: inherit; - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; font-weight: 500; cursor: pointer; box-shadow: 0 4px 14px rgba(0, 0, 0, 0.12); diff --git a/src/features/map/components/MapMobileTabs.tsx b/src/features/map/components/MapMobileTabs.tsx index 42ec6dfb..646dc33d 100644 --- a/src/features/map/components/MapMobileTabs.tsx +++ b/src/features/map/components/MapMobileTabs.tsx @@ -4,7 +4,7 @@ import Link from 'next/link'; import styled from '@emotion/styled'; import { HugeiconsIcon } from '@hugeicons/react' import { BookOpen01Icon, Bookmark01Icon, Film01Icon, HeadphonesIcon, Home01Icon, MapIcon } from '@hugeicons/core-free-icons'; -import { lightPalette, meok, fontSize } from '@/design-system/tokens'; +import { lightPalette, meok, typographyRole } from '@/design-system/tokens'; import { useMapStore } from '@/features/map/hooks/useMapStore'; import { useBookmarkStore } from '@/features/map/hooks/useBookmarkStore'; @@ -28,7 +28,7 @@ const TabButton = styled.button<{ $active?: boolean }>` background: none; color: ${({ $active }) => ($active ? lightPalette.hwanggeum[700] : 'rgba(33, 30, 25, 0.68)')}; font-family: 'Spoqa Han Sans Neo', sans-serif; - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; font-weight: ${({ $active }) => ($active ? 600 : 400)}; letter-spacing: -0.02em; cursor: pointer; @@ -59,7 +59,7 @@ const TabLink = styled(Link)` padding: 0; color: rgba(33, 30, 25, 0.68); font-family: 'Spoqa Han Sans Neo', sans-serif; - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; font-weight: 400; letter-spacing: -0.02em; text-decoration: none; @@ -104,7 +104,7 @@ const CountBadge = styled.span` background: ${lightPalette.hwanggeum[400]}; } } - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; font-weight: 700; line-height: 1; `; diff --git a/src/features/map/components/MapNavRail.tsx b/src/features/map/components/MapNavRail.tsx index 59916604..dadefdf6 100644 --- a/src/features/map/components/MapNavRail.tsx +++ b/src/features/map/components/MapNavRail.tsx @@ -11,7 +11,7 @@ import { HanokIcon } from './HanokIcon' import { transientProps } from '@/design-system/styled'; import { useMapStore } from '@/features/map/hooks/useMapStore'; import { useJourneyStore } from '@/features/journey-curator/store/useJourneyStore'; -import { lightPalette, meok, fontSize, ringShadow } from '@/design-system/tokens'; +import { meok, ringShadow } from '@/design-system/tokens'; import { RAIL_ENTER_DELAY_S, RAIL_ENTER_DURATION_S, ENTRANCE_EASE } from '@/shared/navigation/mapEntranceTiming'; import { useMapEntranceStore } from '@/shared/navigation/mapEntranceState'; diff --git a/src/features/map/components/PlaceList.tsx b/src/features/map/components/PlaceList.tsx index 4326c953..94d7fba2 100644 --- a/src/features/map/components/PlaceList.tsx +++ b/src/features/map/components/PlaceList.tsx @@ -5,7 +5,7 @@ import styled from '@emotion/styled'; import { keyframes } from '@emotion/react'; import { HugeiconsIcon } from '@hugeicons/react' import { Cancel01Icon, ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon, MapIcon, RotateCcwIcon, AlertCircleIcon, SparklesIcon, ListIcon, Bookmark01Icon, GridViewIcon } from '@hugeicons/core-free-icons' -import { lightPalette, meok, surface, fontSize, ringShadow } from '@/design-system/tokens'; +import { lightPalette, meok, surface, typographyRole, ringShadow } from '@/design-system/tokens'; import { useMapStore } from '@/features/map/hooks/useMapStore'; import { useBookmarkStore } from '@/features/map/hooks/useBookmarkStore'; import { distanceInMeters } from '@/features/map/utils/geo'; @@ -69,7 +69,7 @@ const DegradedNotice = styled.div` background: #f8f8f7; padding: 10px 12px; color: ${meok[700]}; - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; line-height: 1.4; [data-theme='dark'] & { @@ -130,7 +130,7 @@ const SortSelect = styled.select` box-shadow: none !important; font-family: inherit; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 500; color: ${meok[700]}; padding: 4px 24px 4px 10px; @@ -192,7 +192,7 @@ const PageNavBtn = styled.button` background: rgba(78, 89, 104, 0.06); color: ${meok[500]}; font-family: inherit; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 400; cursor: pointer; transition: all 0.15s ease; @@ -237,7 +237,7 @@ const PageNumberBtn = styled.button<{ $active: boolean }>` background: ${({ $active }) => ($active ? meok[900] : 'transparent')}; color: ${({ $active }) => ($active ? '#ffffff' : meok[700])}; font-family: inherit; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: ${({ $active }) => ($active ? 700 : 500)}; cursor: pointer; transition: all 0.15s ease; @@ -343,14 +343,14 @@ const EmptyIconBox = styled.div` const EmptyTitle = styled.h4` margin: 0 0 6px; - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; font-weight: 500; color: ${meok[900]}; `; const EmptyDesc = styled.p` margin: 0 0 18px; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 400; color: ${meok[500]}; line-height: 1.5; @@ -367,7 +367,7 @@ const ActionButton = styled.button` background: rgba(78, 89, 104, 0.08); color: ${meok[700]}; font-family: inherit; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 500; cursor: pointer; transition: all 0.15s ease; diff --git a/src/features/map/components/PlaceListItem.tsx b/src/features/map/components/PlaceListItem.tsx index 52083122..21ed8a4b 100644 --- a/src/features/map/components/PlaceListItem.tsx +++ b/src/features/map/components/PlaceListItem.tsx @@ -6,7 +6,7 @@ import styled from '@emotion/styled'; import { motion } from 'framer-motion'; import { HugeiconsIcon } from '@hugeicons/react' import { BookOpen01Icon, Calendar01Icon, Coffee01Icon, HeadphonesIcon, Home01Icon, LandmarkIcon, ShoppingBag01Icon, SparklesIcon, Store01Icon, UtensilsIcon } from '@hugeicons/core-free-icons' -import { lightPalette, meok, fontSize } from '@/design-system/tokens'; +import { lightPalette, meok, typographyRole } from '@/design-system/tokens'; import { useMapStore } from '@/features/map/hooks/useMapStore'; import { useSorimaruAudioStore } from '@/features/sorimaru-audio/store/useSorimaruAudioStore'; import { matchSorimaruStory } from '@/features/sorimaru-audio/hooks/useSorimaruPlaceStory'; @@ -148,7 +148,7 @@ const Row1 = styled.div` const Title = styled.h4<{ $isSelected?: boolean }>` margin: 0; - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; font-weight: 500; color: ${meok[900]}; overflow: hidden; @@ -175,7 +175,7 @@ const CategoryTag = styled.span<{ $category: PlaceCategory }>` align-items: center; padding: 2px 7px; border-radius: 6px; - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; font-weight: 600; line-height: 1.4; color: ${({ $category }) => CATEGORY_STYLES[$category]?.main || meok[700]}; @@ -198,7 +198,7 @@ const SorimaruBadge = styled.span` gap: 3px; padding: 2px 6.5px; border-radius: 6px; - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; font-weight: 700; line-height: 1.4; color: ${lightPalette.jangmi[700]}; @@ -211,7 +211,7 @@ const MetaRow = styled.div` display: flex; align-items: center; gap: 4px; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; color: ${meok[500]}; white-space: nowrap; overflow: hidden; @@ -221,7 +221,7 @@ const MetaRow = styled.div` const DistanceHighlight = styled.span` font-weight: 700; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 500; color: ${meok[700]}; font-variant-numeric: tabular-nums; @@ -235,7 +235,7 @@ const DistanceHighlight = styled.span` const TravelTimeText = styled.span` font-weight: 500; color: ${meok[500]}; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; [data-theme='dark'] & { color: ${meok[400]}; @@ -244,7 +244,7 @@ const TravelTimeText = styled.span` const DotDivider = styled.span` color: ${meok[400]}; - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; padding: 0 1px; `; diff --git a/src/features/map/components/RegionChips.tsx b/src/features/map/components/RegionChips.tsx index 3bb6450d..75536ebd 100644 --- a/src/features/map/components/RegionChips.tsx +++ b/src/features/map/components/RegionChips.tsx @@ -2,7 +2,7 @@ import { useState } from 'react'; import styled from '@emotion/styled'; -import { meok, surface, fontSize, ringShadow } from '@/design-system/tokens'; +import { meok, surface, typographyRole, ringShadow } from '@/design-system/tokens'; import { snapshotFromMap } from '@/features/map/hooks/useKakaoMap'; import { DEFAULT_CENTER, useMapStore } from '@/features/map/hooks/useMapStore'; import type { LatLng } from '@/features/map/types'; @@ -40,7 +40,7 @@ const Chip = styled.button<{ $active: boolean }>` background: ${({ $active }) => ($active ? meok[900] : 'transparent')}; color: ${({ $active }) => ($active ? surface.light.card : meok[700])}; font-family: inherit; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 500; white-space: nowrap; cursor: pointer; diff --git a/src/features/map/components/SearchBar.tsx b/src/features/map/components/SearchBar.tsx index a85fbd7f..cf7995da 100644 --- a/src/features/map/components/SearchBar.tsx +++ b/src/features/map/components/SearchBar.tsx @@ -2,7 +2,7 @@ import React, { useState, useCallback, useEffect, useRef } from 'react'; import styled from '@emotion/styled'; import { HugeiconsIcon } from '@hugeicons/react' import { GlobeIcon, Search01Icon } from '@hugeicons/core-free-icons' -import { lightPalette, meok, surface, fontSize, ringShadow } from '@/design-system/tokens'; +import { lightPalette, meok, surface, typographyRole, ringShadow } from '@/design-system/tokens'; import { snapshotFromMap } from '@/features/map/hooks/useKakaoMap'; import { DEFAULT_CENTER, DEFAULT_LEVEL, useMapStore } from '@/features/map/hooks/useMapStore'; @@ -62,7 +62,7 @@ const Input = styled.input` -webkit-appearance: none; appearance: none; font-family: inherit; - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; font-weight: 500; color: ${meok[900]}; text-overflow: ellipsis; @@ -135,7 +135,7 @@ const ResetAllBtn = styled.button` background: rgba(25, 31, 40, 0.03); border: none; color: ${meok[500]}; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 500; cursor: pointer; transition: all 0.15s ease; @@ -161,7 +161,7 @@ const GroupTitle = styled.div` display: flex; align-items: center; gap: 5px; - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; font-weight: 500; color: ${meok[500]}; letter-spacing: 0.3px; @@ -188,7 +188,7 @@ const Suggestion = styled.button` background: rgba(25, 31, 40, 0.04); color: ${meok[700]}; font-family: inherit; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 400; cursor: pointer; border: none; diff --git a/src/features/map/components/detail/PlaceDetail.styles.ts b/src/features/map/components/detail/PlaceDetail.styles.ts index cd4e7645..659c0d9c 100644 --- a/src/features/map/components/detail/PlaceDetail.styles.ts +++ b/src/features/map/components/detail/PlaceDetail.styles.ts @@ -1,6 +1,6 @@ import styled from '@emotion/styled'; import { keyframes } from '@emotion/react'; -import { lightPalette, darkPalette, palette, meok, surface, fontSize, fontFamily } from '@/design-system/tokens'; +import { lightPalette, darkPalette, palette, meok, surface, typographyRole } from '@/design-system/tokens'; export const pulse = keyframes` 0%, 100% { opacity: 1; } @@ -50,7 +50,7 @@ export const HeaderBadge = styled.div` border-radius: 9999px; background: rgba(78, 89, 104, 0.08); color: ${meok[700]}; - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; font-weight: 500; letter-spacing: 0.06em; border: none; @@ -70,7 +70,7 @@ export const BackToPopularBtn = styled.button` background: rgba(78, 89, 104, 0.08); color: ${meok[900]}; font-family: inherit; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 500; border: none; cursor: pointer; @@ -157,7 +157,7 @@ export const PlaceTitle = styled.h2` export const PlaceAddress = styled.p` margin: 0 0 10px; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; color: ${meok[500]}; [data-theme='dark'] & { @@ -174,7 +174,7 @@ export const BadgeRow = styled.div` export const Badge = styled.span` padding: 3px 9px; border-radius: 9999px; - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; font-weight: 400; color: ${meok[700]}; background: rgba(78, 89, 104, 0.07); @@ -199,7 +199,7 @@ export const SmartFeatureChip = styled.span<{ $type?: 'free' | 'parking' | 'audi gap: 4px; padding: 3px 8px; border-radius: 8px; - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; font-weight: 500; color: ${({ $type }) => $type === 'audio' ? lightPalette.jangmi[700] : meok[700]}; @@ -248,7 +248,7 @@ export const LiveWarmthStatus = styled.div` display: flex; align-items: center; gap: 7px; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 500; color: ${meok[900]}; min-width: 0; @@ -338,7 +338,7 @@ export const StampBannerTitle = styled.span` `; export const StampBannerSub = styled.span` - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; color: ${meok[500]}; [data-theme='dark'] & { @@ -353,7 +353,7 @@ export const StampActionBtn = styled.button<{ $isVisited: boolean }>` padding: 6px 12px; border: none; border-radius: 8px; - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; font-weight: 700; cursor: pointer; @@ -386,7 +386,7 @@ export const StampActionBtn = styled.button<{ $isVisited: boolean }>` `; export const LiveWarmthCount = styled.span` - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; color: ${palette.juhong[600]}; font-weight: 700; flex-shrink: 0; @@ -473,7 +473,7 @@ export const HeroActionTile = styled.button<{ $highlight?: boolean; $isWarmth?: } span { - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; font-weight: 500; white-space: nowrap; } @@ -505,7 +505,7 @@ export const HeroActionLink = styled.a` } span { - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; font-weight: 500; white-space: nowrap; } @@ -534,7 +534,7 @@ export const CoreRow = styled.div` `; export const CoreLabel = styled.span` - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; color: ${meok[500]}; flex-shrink: 0; @@ -544,7 +544,7 @@ export const CoreLabel = styled.span` `; export const CoreValue = styled.span` - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; font-weight: 500; color: ${meok[900]}; text-align: right; @@ -556,7 +556,7 @@ export const CoreValue = styled.span` `; export const BuildingSpecValue = styled.span` - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; font-weight: 600; color: ${palette.juhong[500]}; text-align: right; @@ -613,7 +613,7 @@ export const ToggleMoreBtn = styled.button` background: transparent; color: var(--color-action-secondary); font-family: inherit; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 400; cursor: pointer; @@ -655,7 +655,7 @@ export const BookmarkButton = styled.button<{ $active: boolean }>` color: ${({ $active }) => $active ? palette.juhong[500] : meok[900]}; font-family: inherit; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 500; cursor: pointer; transition: all 0.15s ease; @@ -689,7 +689,7 @@ export const ShareButton = styled.button` background: rgba(78, 89, 104, 0.08); color: ${meok[900]}; font-family: inherit; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 400; cursor: pointer; transition: background 0.15s ease; @@ -724,7 +724,7 @@ export const NavButton = styled.a` background: ${meok[900]}; color: #ffffff; font-family: inherit; - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; font-weight: 700; text-decoration: none; diff --git a/src/features/map/components/detail/PlaceDetailCarousel.tsx b/src/features/map/components/detail/PlaceDetailCarousel.tsx index eb02bb8c..30ce2002 100644 --- a/src/features/map/components/detail/PlaceDetailCarousel.tsx +++ b/src/features/map/components/detail/PlaceDetailCarousel.tsx @@ -5,7 +5,7 @@ import styled from '@emotion/styled'; import { keyframes } from '@emotion/react'; import { HugeiconsIcon } from '@hugeicons/react' import { ChevronLeftIcon, ChevronRightIcon, LandmarkIcon, Home01Icon, UtensilsIcon, Coffee01Icon, ShoppingBag01Icon } from '@hugeicons/core-free-icons' -import { meok , fontSize } from '@/design-system/tokens'; +import { meok, typographyRole } from '@/design-system/tokens'; import type { PlaceCategory } from '@/features/map/types'; const ImageContainer = styled.div` @@ -135,7 +135,7 @@ const FallbackIconWrap = styled.div` `; const FallbackText = styled.span` - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 500; color: ${meok[700]}; letter-spacing: -0.2px; diff --git a/src/features/map/components/detail/RoadviewModal.tsx b/src/features/map/components/detail/RoadviewModal.tsx index 51513618..6c4bb2f1 100644 --- a/src/features/map/components/detail/RoadviewModal.tsx +++ b/src/features/map/components/detail/RoadviewModal.tsx @@ -1,10 +1,10 @@ -'use client'; +'use client'; import React, { useEffect, useRef, useState } from 'react'; import styled from '@emotion/styled'; import { HugeiconsIcon } from '@hugeicons/react' import { Camera01Icon, Cancel01Icon, AlertCircleIcon } from '@hugeicons/core-free-icons' -import { meok, lightPalette, darkPalette, surface, fontSize } from '@/design-system/tokens'; +import { meok, lightPalette, surface, typographyRole } from '@/design-system/tokens'; import { livelyModalEnter } from '@/shared/motion/modalMotion'; const Backdrop = styled.div` @@ -70,7 +70,7 @@ const TitleBox = styled.div` `; const Title = styled.h3` - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; font-weight: 500; color: ${meok[900]}; margin: 0; @@ -88,7 +88,7 @@ const SubBadge = styled.span` border-radius: 9999px; background: var(--color-action-secondary-bg); color: var(--color-action-secondary); - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; font-weight: 700; `; @@ -142,7 +142,7 @@ const FallbackOverlay = styled.div` background: ${surface.dark.app}; p { - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; color: ${meok[400]}; margin: 0; max-width: 320px; diff --git a/src/features/map/components/feed/FestivalExhibitionCarousel.tsx b/src/features/map/components/feed/FestivalExhibitionCarousel.tsx index 60dbb1af..0d920384 100644 --- a/src/features/map/components/feed/FestivalExhibitionCarousel.tsx +++ b/src/features/map/components/feed/FestivalExhibitionCarousel.tsx @@ -62,7 +62,7 @@ const SkeletonBar = styled.div<{ $w: string; $h: string }>` `; import { HugeiconsIcon } from '@hugeicons/react' import { Calendar01Icon, ChevronLeftIcon, ChevronRightIcon, SparklesIcon } from '@hugeicons/core-free-icons' -import { lightPalette, darkPalette, meok, surface, fontSize, fontFamily } from '@/design-system/tokens'; +import { lightPalette, darkPalette, meok, typographyRole } from '@/design-system/tokens'; import { useMapStore } from '@/features/map/hooks/useMapStore'; import { isHanok } from '@/features/map/utils/isHanok'; import { EmphasisBadge } from '@/features/map/components/EmphasisBadge'; @@ -100,7 +100,7 @@ const TitleGroup = styled.div` const SectionTitle = styled.h3` margin: 0; - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; font-weight: 700; color: ${meok[900]}; letter-spacing: -0.02em; @@ -111,7 +111,7 @@ const SectionTitle = styled.h3` `; const BadgeTitle = styled.span` - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; font-weight: 500; color: ${lightPalette.jaha[600]}; @@ -126,7 +126,7 @@ const MoreBtn = styled.button` gap: 2px; background: transparent; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 500; color: ${lightPalette.jaha[600]}; cursor: pointer; @@ -284,7 +284,7 @@ const CardBadge = styled.div` gap: 3px; padding: 2.5px 7.5px; border-radius: 9999px; - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; font-weight: 700; color: #ffffff; background: ${lightPalette.jaha[600]}; @@ -301,7 +301,7 @@ const CardBody = styled.div` const CardTitle = styled.h4` margin: 0 0 4px; - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; font-weight: 500; color: ${meok[900]}; white-space: nowrap; @@ -318,7 +318,7 @@ const CardDateRow = styled.div` display: flex; align-items: center; gap: 4px; - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; color: ${lightPalette.jaha[600]}; font-weight: 500; @@ -329,7 +329,7 @@ const CardDateRow = styled.div` const CardAddr = styled.div` margin-top: 3px; - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; font-weight: 400; color: ${meok[400]}; white-space: nowrap; diff --git a/src/features/map/components/feed/LiveNoticeBanner.tsx b/src/features/map/components/feed/LiveNoticeBanner.tsx index 6a717b6a..5f1e9889 100644 --- a/src/features/map/components/feed/LiveNoticeBanner.tsx +++ b/src/features/map/components/feed/LiveNoticeBanner.tsx @@ -4,7 +4,7 @@ import React, { useState, useEffect } from 'react'; import styled from '@emotion/styled'; import { HugeiconsIcon } from '@hugeicons/react' import { Megaphone01Icon } from '@hugeicons/core-free-icons' -import { lightPalette, meok , fontSize } from '@/design-system/tokens'; +import { lightPalette, meok, typographyRole } from '@/design-system/tokens'; const NOTICES = [ '지도에서 전국 한옥 공간과 전통 체험을 찾아보세요', @@ -60,7 +60,7 @@ const TextScroller = styled.div` const NoticeText = styled.p` margin: 0; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 500; color: ${meok[900]}; white-space: nowrap; diff --git a/src/features/map/components/feed/SmartAroundFeed.tsx b/src/features/map/components/feed/SmartAroundFeed.tsx index 98028ad8..044ef062 100644 --- a/src/features/map/components/feed/SmartAroundFeed.tsx +++ b/src/features/map/components/feed/SmartAroundFeed.tsx @@ -63,7 +63,7 @@ const SkeletonBar = styled.div<{ $w: string; $h: string; $radius?: string }>` `; import { HugeiconsIcon } from '@hugeicons/react' import { ChevronLeftIcon, ChevronRightIcon } from '@hugeicons/core-free-icons' -import { lightPalette, meok, fontSize, fontFamily } from '@/design-system/tokens'; +import { meok, typographyRole } from '@/design-system/tokens'; import { useMapStore } from '@/features/map/hooks/useMapStore'; import { CATEGORY_STYLES } from '../PlaceMarkers'; import { isHanok } from '@/features/map/utils/isHanok'; @@ -111,7 +111,7 @@ const Title = styled.h3` `; const SubText = styled.span` - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; font-weight: 500; color: ${meok[500]}; `; @@ -251,7 +251,7 @@ const DistanceOverlay = styled.div` background: rgba(25, 31, 40, 0.68); backdrop-filter: blur(4px); color: #ffffff; - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; font-weight: 500; `; @@ -264,7 +264,7 @@ const Body = styled.div` const Name = styled.h4` margin: 0; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 500; color: ${meok[900]}; white-space: nowrap; @@ -279,7 +279,7 @@ const Name = styled.h4` const MoodReview = styled.p` margin: 1px 0 4px; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 400; color: ${meok[500]}; white-space: nowrap; @@ -298,7 +298,7 @@ const TagRow = styled.div` const HashTag = styled.span<{ $category?: PlaceCategory }>` display: inline-flex; align-items: center; - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; font-weight: 500; color: ${({ $category }) => $category ? CATEGORY_STYLES[$category]?.main : meok[600]}; diff --git a/src/features/map/components/feed/SorimaruSpotlightBanner.tsx b/src/features/map/components/feed/SorimaruSpotlightBanner.tsx index 9992953e..b4bb4316 100644 --- a/src/features/map/components/feed/SorimaruSpotlightBanner.tsx +++ b/src/features/map/components/feed/SorimaruSpotlightBanner.tsx @@ -5,7 +5,7 @@ import styled from '@emotion/styled'; import { keyframes } from '@emotion/react'; import { HugeiconsIcon } from '@hugeicons/react'; import { PlayIcon } from '@hugeicons/core-free-icons'; -import { lightPalette, meok, surface, fontSize } from '@/design-system/tokens'; +import { lightPalette, meok, surface, typographyRole } from '@/design-system/tokens'; const bannerShimmer = keyframes` 0% { background-position: -200% 0; } @@ -65,14 +65,14 @@ const TopRow = styled.div` `; const DurationText = styled.span` - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; font-weight: 500; color: ${meok[500]}; `; const StoryTitle = styled.h4` margin: 0; - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; font-weight: 700; color: ${meok[900]}; letter-spacing: -0.01em; @@ -87,7 +87,7 @@ const StoryTitle = styled.h4` const StoryExcerpt = styled.p` margin: 0 0 12px; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; line-height: 1.4; color: ${meok[500]}; white-space: nowrap; @@ -110,7 +110,7 @@ const DocentTag = styled.div` display: flex; align-items: center; gap: 4px; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 500; color: ${lightPalette.jangmi[700]}; `; @@ -126,7 +126,7 @@ const StartBtn = styled.button` background: ${lightPalette.jangmi[500]}; color: ${surface.light.card}; font-family: inherit; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 700; cursor: pointer; transition: all 0.15s cubic-bezier(0.16, 1, 0.3, 1); diff --git a/src/features/sorimaru-audio/components/AllStoriesModal.tsx b/src/features/sorimaru-audio/components/AllStoriesModal.tsx index 8a9bbfee..433d8836 100644 --- a/src/features/sorimaru-audio/components/AllStoriesModal.tsx +++ b/src/features/sorimaru-audio/components/AllStoriesModal.tsx @@ -8,7 +8,7 @@ import { Cancel01Icon, Search01Icon, Clock01Icon, PauseIcon, PlayIcon } from '@h import { useSorimaruAudioStore } from '@/features/sorimaru-audio/store/useSorimaruAudioStore'; import type { SorimaruStorySummary } from '@/features/sorimaru-audio/domain/sorimaruStory'; import type { SorimaruCategory } from '@/features/sorimaru-audio/types/sorimaru.types'; -import { palette, meok, surface, fontSize } from '@/design-system/tokens'; +import { palette, meok, surface, typographyRole } from '@/design-system/tokens'; import { livelyModalEnter } from '@/shared/motion/modalMotion'; import { OniSearchEmpty } from '@/shared/components/OniSearchEmpty'; @@ -65,7 +65,7 @@ const ModalHeader = styled.div` `; const HeaderBadge = styled.span` - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; font-weight: 700; color: ${palette.juhong[400]}; text-transform: uppercase; @@ -76,13 +76,9 @@ const HeaderBadge = styled.span` const HeaderTitle = styled.h2` font-family: inherit; - font-size: ${fontSize.xl}; + font-size: ${typographyRole.sectionTitle.fontSize}; font-weight: 600; color: #ffffff; - - @media (min-width: 640px) { - font-size: ${fontSize['2xl']}; - } `; const CloseButton = styled.button` @@ -128,7 +124,7 @@ const CategoryChipsRail = styled.div` const ChipButton = styled.button<{ isSelected: boolean }>` padding: 0.375rem 0.875rem; border-radius: 9999px; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 700; white-space: nowrap; border: 1px solid ${(props) => (props.isSelected ? palette.juhong[500] : 'rgba(255, 255, 255, 0.1)')}; @@ -154,7 +150,7 @@ const SearchInput = styled.input` border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 0.75rem; font-family: inherit; - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; color: #ffffff; outline: none; @@ -189,7 +185,7 @@ const EmptyState = styled.div` padding: 3rem 0; text-align: center; color: ${meok[500]}; - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; `; const StoryCard = styled.div<{ isCurrent: boolean }>` @@ -237,7 +233,7 @@ const MetaRow = styled.div` const CategoryTag = styled.span` padding: 0.125rem 0.5rem; - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; font-weight: 700; background-color: ${palette.juhong[500]}; color: #ffffff; @@ -245,7 +241,7 @@ const CategoryTag = styled.span` `; const DurationText = styled.span` - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; color: rgba(255, 255, 255, 0.6); display: flex; align-items: center; @@ -253,7 +249,7 @@ const DurationText = styled.span` `; const StoryTitle = styled.h4` - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; font-weight: 700; color: #ffffff; overflow: hidden; @@ -261,13 +257,13 @@ const StoryTitle = styled.h4` white-space: nowrap; @media (min-width: 640px) { - font-size: ${fontSize.base}; + font-size: ${typographyRole.body.fontSize}; } `; const StoryDesc = styled.p` margin-top: 0.125rem; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; color: ${meok[500]}; & .loc { @@ -283,7 +279,7 @@ const StoryDesc = styled.p` overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; } `; @@ -294,7 +290,7 @@ const PlayActionBtn = styled.button<{ isPlaying: boolean }>` display: flex; align-items: center; justify-content: center; - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; font-weight: 700; flex-shrink: 0; margin-left: 0.75rem; @@ -441,7 +437,7 @@ export const AllStoriesModal: React.FC = ({ border: '1px solid rgba(255, 255, 255, 0.15)', background: 'rgba(255, 255, 255, 0.08)', color: meok[200], - fontSize: fontSize.xs, + fontSize: typographyRole.label.fontSize, cursor: 'pointer', }} > diff --git a/src/features/sorimaru-audio/components/SavedSoundDrawer.tsx b/src/features/sorimaru-audio/components/SavedSoundDrawer.tsx index 9a661eec..1cf99acc 100644 --- a/src/features/sorimaru-audio/components/SavedSoundDrawer.tsx +++ b/src/features/sorimaru-audio/components/SavedSoundDrawer.tsx @@ -7,7 +7,7 @@ import { HugeiconsIcon } from '@hugeicons/react' import { HeartIcon, Cancel01Icon } from '@hugeicons/core-free-icons' import type { SorimaruStorySummary } from '@/features/sorimaru-audio/domain/sorimaruStory'; import { useSorimaruAudioStore } from '@/features/sorimaru-audio/store/useSorimaruAudioStore'; -import { palette, meok, surface, fontSize } from '@/design-system/tokens'; +import { palette, meok, surface, typographyRole } from '@/design-system/tokens'; interface SavedSoundDrawerProps { savedStories: SorimaruStorySummary[]; @@ -206,7 +206,7 @@ export const SavedSoundDrawer: React.FC = ({ > 마음 담아둔 소리 - + {savedStories.length} @@ -276,7 +276,7 @@ export const SavedSoundDrawer: React.FC = ({ />
-

+

{story.region.name || '소리의 장소'}

@@ -295,14 +295,14 @@ export const SavedSoundDrawer: React.FC = ({ @@ -314,7 +314,7 @@ export const SavedSoundDrawer: React.FC = ({ )} -

+

이 기기에만 저장돼요.

diff --git a/src/features/sorimaru-audio/components/SorimaruArchiveBrowse.tsx b/src/features/sorimaru-audio/components/SorimaruArchiveBrowse.tsx index b443b796..618a617e 100644 --- a/src/features/sorimaru-audio/components/SorimaruArchiveBrowse.tsx +++ b/src/features/sorimaru-audio/components/SorimaruArchiveBrowse.tsx @@ -9,7 +9,7 @@ import { useSorimaruAudioStore } from '@/features/sorimaru-audio/store/useSorima import type { SorimaruStorySummary } from '@/features/sorimaru-audio/domain/sorimaruStory'; import { groupSorimaruStoriesByPlace, type SorimaruPlaceGroup } from '@/features/sorimaru-audio/utils/sorimaruArchiveGrouping'; import { useSorimaruImage } from '@/features/sorimaru-audio/hooks/useSorimaruImage'; -import { palette, meok, surface, fontSize, ringShadow } from '@/design-system/tokens'; +import { palette, meok, surface, typographyRole, ringShadow } from '@/design-system/tokens'; import { OniSearchEmpty } from '@/shared/components/OniSearchEmpty'; import { SorimaruRequestErrorState } from '@/features/sorimaru-audio/presentation/SorimaruRequestErrorState'; @@ -301,7 +301,7 @@ const TitleRow = styled.div` const StoryRowTitle = styled.h3<{ $isCurrent: boolean }>` font-family: var(--font-hanok); - font-size: ${fontSize.base}; + font-size: ${typographyRole.body.fontSize}; font-weight: 700; line-height: 1.3; letter-spacing: -0.02em; @@ -327,7 +327,7 @@ const StoryRowTitle = styled.h3<{ $isCurrent: boolean }>` const DurationPill = styled.span` flex-shrink: 0; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 600; color: ${meok[500]}; background-color: rgba(0, 0, 0, 0.04); @@ -345,7 +345,7 @@ const LocationMeta = styled.div` display: flex; align-items: center; gap: 4px; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; color: ${meok[600]}; overflow: hidden; text-overflow: ellipsis; @@ -361,7 +361,7 @@ const HashtagsRow = styled.div` flex-wrap: wrap; align-items: center; gap: 0.2rem 0.45rem; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; line-height: 1.3; margin-top: 0.05rem; `; @@ -647,7 +647,7 @@ export function SorimaruArchiveBrowse({ stories, isLoading, error = null, onRetr {view === 'places' && ( - + 현재 결과 기준 )} diff --git a/src/features/sorimaru-audio/components/SorimaruAudioFeature.tsx b/src/features/sorimaru-audio/components/SorimaruAudioFeature.tsx index 8a81a116..25e2eeee 100644 --- a/src/features/sorimaru-audio/components/SorimaruAudioFeature.tsx +++ b/src/features/sorimaru-audio/components/SorimaruAudioFeature.tsx @@ -30,7 +30,7 @@ import { useSorimaruDetailSelection } from '@/features/sorimaru-audio/hooks/useS import { useSorimaruRegionStories } from '@/features/sorimaru-audio/presentation/hooks/useSorimaruRegionStories'; import { useViewportActivation } from '@/shared/hooks/useViewportActivation'; import { SOUND_CONSTELLATION_API_ROOT_MARGIN } from '@/private/core-ui/sorimaru/soundConstellationMotion'; -import { palette, meok, surface, fontSize } from '@/design-system/tokens'; +import { palette, meok, surface, typographyRole } from '@/design-system/tokens'; import { HanjiDeckleEdge } from '@/shared/components/HanjiDeckleEdge'; import { NearbyLocationDescription } from '@/features/sorimaru-audio/presentation/NearbyLocationDescription'; @@ -258,7 +258,7 @@ const LocationButton = styled.button` border-radius: 9999px; background-color: rgba(255, 255, 255, 0.55); padding: 0 0.75rem; - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; font-weight: 500; color: ${meok[700]}; border: 1px solid rgba(33, 30, 25, 0.08); @@ -556,7 +556,7 @@ export const SorimaruAudioFeature: React.FC = ({
- + {locationLabel} 내 주변 오디오 {nearbyStories.length}개 diff --git a/src/features/sorimaru-audio/components/SorimaruFooterCTA.tsx b/src/features/sorimaru-audio/components/SorimaruFooterCTA.tsx index c5df5ad4..f85edc10 100644 --- a/src/features/sorimaru-audio/components/SorimaruFooterCTA.tsx +++ b/src/features/sorimaru-audio/components/SorimaruFooterCTA.tsx @@ -1,319 +1,315 @@ -'use client'; - -import React, { useMemo } from 'react'; -import Link from 'next/link'; -import styled from '@emotion/styled'; -import { keyframes } from '@emotion/react'; -import { motion } from 'framer-motion'; -import SHADOW from '@/data/solarShadow.json'; -import { palette, meok, surface, fontSize } from '@/design-system/tokens'; - -function getNextSolarTerm(now = new Date()) { - const year = now.getFullYear(); - const dated = [year, year + 1].flatMap((y) => - (SHADOW.stops || []).map((term) => ({ name: term.name, date: new Date(y, term.month - 1, term.day) })), - ); - const next = dated.filter((term) => term.date >= now).sort((a, b) => a.date.getTime() - b.date.getTime())[0]; - if (!next) return null; - return { - name: next.name, - daysLeft: Math.ceil((next.date.getTime() - now.getTime()) / 86400000), - }; -} - -const pulseAnimation = keyframes` - 0%, 100% { opacity: 1; } - 50% { opacity: 0.5; } -`; - -const SectionContainer = styled.section` - width: 100%; - padding: 3rem 0; - @media (min-width: 640px) { - padding: 4rem 0; - } -`; - -const ContentWrapper = styled.div` - width: 100%; - text-align: center; -`; - -const GlassCard = styled(motion.div)` - position: relative; - overflow: hidden; - border-radius: 1.5rem; - background: linear-gradient(to bottom right, #ffffff, ${palette.juhong[50]}); - padding: 2rem; - color: ${meok[900]}; - backdrop-filter: blur(12px); - - [data-theme='dark'] & { - background: linear-gradient(to bottom right, ${surface.dark.card}, ${surface.dark.surface}); - color: ${meok[100]}; - border: 1px solid rgba(255, 255, 255, 0.08); - } - - @media (min-width: 640px) { - padding: 3.5rem; - } -`; - -const RadialOverlay = styled.div` - pointer-events: none; - position: absolute; - inset: 0; - background: radial-gradient(circle, rgba(255, 255, 255, 0.6) 0%, transparent 60%, rgba(255, 244, 235, 0.8) 100%); - - [data-theme='dark'] & { - background: radial-gradient(circle, rgba(33, 39, 52, 0.4) 0%, transparent 60%, rgba(23, 30, 43, 0.8) 100%); - } -`; - -const GlowOrbRight = styled.div` - pointer-events: none; - position: absolute; - top: -4rem; - right: -4rem; - width: 14rem; - height: 14rem; - border-radius: 9999px; - background: rgba(255, 85, 0, 0.1); - filter: blur(48px); -`; - -const GlowOrbLeft = styled.div` - pointer-events: none; - position: absolute; - bottom: -4rem; - left: -4rem; - width: 14rem; - height: 14rem; - border-radius: 9999px; - background: rgba(255, 120, 48, 0.12); - filter: blur(48px); -`; - -const InnerBody = styled.div` - position: relative; - z-index: 10; - display: flex; - flex-direction: column; - align-items: center; -`; - -const SolarBadge = styled.div` - display: inline-flex; - align-items: center; - gap: 0.375rem; - border-radius: 9999px; - background: ${palette.juhong[50]}; - padding: 0.25rem 0.875rem; - font-size: ${fontSize.xs}; - font-weight: 700; - color: ${palette.juhong[500]}; - backdrop-filter: blur(4px); - - [data-theme='dark'] & { - background: rgba(255, 85, 0, 0.15); - color: ${palette.juhong[400]}; - border: 1px solid rgba(255, 85, 0, 0.25); - } -`; - -const PulseDot = styled.span` - width: 0.375rem; - height: 0.375rem; - border-radius: 9999px; - background: ${palette.juhong[500]}; - animation: ${pulseAnimation} 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; -`; - -const MainHeading = styled.h2` - margin-top: 1.25rem; - font-family: inherit; - font-size: ${fontSize['2xl']}; - font-weight: 800; - letter-spacing: -0.025em; - color: ${meok[900]}; - - [data-theme='dark'] & { - color: #ffffff; - } - - @media (min-width: 640px) { - font-size: ${fontSize['4xl']}; - } -`; - -const Description = styled.p` - margin-top: 0.875rem; - max-width: 36rem; - font-size: ${fontSize.xs}; - line-height: 1.625; - color: ${meok[700]}; - - [data-theme='dark'] & { - color: ${meok[400]}; - } - - @media (min-width: 640px) { - font-size: ${fontSize.sm}; - } -`; - -const ButtonRow = styled.div` - margin-top: 2rem; - display: flex; - width: 100%; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 0.875rem; - @media (min-width: 640px) { - width: auto; - flex-direction: row; - } -`; - -const PrimaryCtaLink = styled(Link)` - position: relative; - display: inline-flex; - width: 100%; - align-items: center; - justify-content: center; - gap: 0.5rem; - overflow: hidden; - border-radius: 9999px; - background: linear-gradient(to right, ${palette.juhong[500]}, ${palette.juhong[700]}); - padding: 0.875rem 1.75rem; - font-size: ${fontSize.xs}; - font-weight: 700; - color: #ffffff; - text-decoration: none; - transition: all 0.3s ease; - - &:hover { - transform: translateY(-2px); - background: linear-gradient(to right, ${palette.juhong[600]}, ${palette.juhong[800]}); - } - - @media (min-width: 640px) { - width: auto; - } - - & .arrow { - display: inline-block; - transition: transform 0.3s ease; - } - - &:hover .arrow { - transform: translateX(4px); - } -`; - -const SecondaryCtaLink = styled(Link)` - display: inline-flex; - width: 100%; - align-items: center; - justify-content: center; - border-radius: 9999px; - background: #ffffff; - padding: 0.875rem 1.75rem; - font-size: ${fontSize.xs}; - font-weight: 700; - color: ${palette.juhong[500]}; - text-decoration: none; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); - transition: all 0.3s ease; - - &:hover { - transform: translateY(-2px); - background: ${palette.juhong[50]}; - } - - [data-theme='dark'] & { - background: ${surface.dark.surface}; - color: ${meok[100]}; - border: 1px solid rgba(255, 255, 255, 0.1); - - &:hover { - background: ${surface.dark.elevated}; - color: #ffffff; - } - } - - @media (min-width: 640px) { - width: auto; - } -`; - -const SubText = styled.p` - margin-top: 2rem; - font-size: ${fontSize.micro}; - font-weight: 500; - color: ${meok[500]}; - - [data-theme='dark'] & { - color: ${meok[400]}; - } -`; - -export const SorimaruFooterCTA: React.FC = () => { - const term = useMemo(() => getNextSolarTerm(), []); - - const termText = (() => { - if (!term) return '절기 알림'; - if (term.daysLeft === 0) return `${term.name} 오늘`; - if (term.daysLeft === 1) return `${term.name} 내일`; - return `${term.name}까지 D-${term.daysLeft}`; - })(); - - return ( - - - - {} - - - - - - {} - - - {termText} - - - - 다음 계절에도, 새로운 이야기를 만나요 - - - - 계절과 날짜가 바뀌면 오늘의 대표 이야기도 새롭게 열려요. - 다음에 돌아왔을 때 다른 장소의 온기를 이어서 들어보세요. - - - {} - - - 전국 한옥 지도에서 둘러보기 - → - - - 온마루 3D 한옥 이야기 보러 가기 - - - - 오늘의 소리는 내일 또 다른 장면으로 이어져요. - - - - - ); -}; +'use client'; + +import React, { useMemo } from 'react'; +import Link from 'next/link'; +import styled from '@emotion/styled'; +import { keyframes } from '@emotion/react'; +import { motion } from 'framer-motion'; +import SHADOW from '@/data/solarShadow.json'; +import { palette, meok, surface, typographyRole } from '@/design-system/tokens'; + +function getNextSolarTerm(now = new Date()) { + const year = now.getFullYear(); + const dated = [year, year + 1].flatMap((y) => + (SHADOW.stops || []).map((term) => ({ name: term.name, date: new Date(y, term.month - 1, term.day) })), + ); + const next = dated.filter((term) => term.date >= now).sort((a, b) => a.date.getTime() - b.date.getTime())[0]; + if (!next) return null; + return { + name: next.name, + daysLeft: Math.ceil((next.date.getTime() - now.getTime()) / 86400000), + }; +} + +const pulseAnimation = keyframes` + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +`; + +const SectionContainer = styled.section` + width: 100%; + padding: 3rem 0; + @media (min-width: 640px) { + padding: 4rem 0; + } +`; + +const ContentWrapper = styled.div` + width: 100%; + text-align: center; +`; + +const GlassCard = styled(motion.div)` + position: relative; + overflow: hidden; + border-radius: 1.5rem; + background: linear-gradient(to bottom right, #ffffff, ${palette.juhong[50]}); + padding: 2rem; + color: ${meok[900]}; + backdrop-filter: blur(12px); + + [data-theme='dark'] & { + background: linear-gradient(to bottom right, ${surface.dark.card}, ${surface.dark.surface}); + color: ${meok[100]}; + border: 1px solid rgba(255, 255, 255, 0.08); + } + + @media (min-width: 640px) { + padding: 3.5rem; + } +`; + +const RadialOverlay = styled.div` + pointer-events: none; + position: absolute; + inset: 0; + background: radial-gradient(circle, rgba(255, 255, 255, 0.6) 0%, transparent 60%, rgba(255, 244, 235, 0.8) 100%); + + [data-theme='dark'] & { + background: radial-gradient(circle, rgba(33, 39, 52, 0.4) 0%, transparent 60%, rgba(23, 30, 43, 0.8) 100%); + } +`; + +const GlowOrbRight = styled.div` + pointer-events: none; + position: absolute; + top: -4rem; + right: -4rem; + width: 14rem; + height: 14rem; + border-radius: 9999px; + background: rgba(255, 85, 0, 0.1); + filter: blur(48px); +`; + +const GlowOrbLeft = styled.div` + pointer-events: none; + position: absolute; + bottom: -4rem; + left: -4rem; + width: 14rem; + height: 14rem; + border-radius: 9999px; + background: rgba(255, 120, 48, 0.12); + filter: blur(48px); +`; + +const InnerBody = styled.div` + position: relative; + z-index: 10; + display: flex; + flex-direction: column; + align-items: center; +`; + +const SolarBadge = styled.div` + display: inline-flex; + align-items: center; + gap: 0.375rem; + border-radius: 9999px; + background: ${palette.juhong[50]}; + padding: 0.25rem 0.875rem; + font-size: ${typographyRole.label.fontSize}; + font-weight: 700; + color: ${palette.juhong[500]}; + backdrop-filter: blur(4px); + + [data-theme='dark'] & { + background: rgba(255, 85, 0, 0.15); + color: ${palette.juhong[400]}; + border: 1px solid rgba(255, 85, 0, 0.25); + } +`; + +const PulseDot = styled.span` + width: 0.375rem; + height: 0.375rem; + border-radius: 9999px; + background: ${palette.juhong[500]}; + animation: ${pulseAnimation} 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; +`; + +const MainHeading = styled.h2` + margin-top: 1.25rem; + font-family: inherit; + font-size: ${typographyRole.display.fontSize}; + font-weight: 800; + letter-spacing: -0.025em; + color: ${meok[900]}; + + [data-theme='dark'] & { + color: #ffffff; + } +`; + +const Description = styled.p` + margin-top: 0.875rem; + max-width: 36rem; + font-size: ${typographyRole.label.fontSize}; + line-height: 1.625; + color: ${meok[700]}; + + [data-theme='dark'] & { + color: ${meok[400]}; + } + + @media (min-width: 640px) { + font-size: ${typographyRole.bodySm.fontSize}; + } +`; + +const ButtonRow = styled.div` + margin-top: 2rem; + display: flex; + width: 100%; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 0.875rem; + @media (min-width: 640px) { + width: auto; + flex-direction: row; + } +`; + +const PrimaryCtaLink = styled(Link)` + position: relative; + display: inline-flex; + width: 100%; + align-items: center; + justify-content: center; + gap: 0.5rem; + overflow: hidden; + border-radius: 9999px; + background: linear-gradient(to right, ${palette.juhong[500]}, ${palette.juhong[700]}); + padding: 0.875rem 1.75rem; + font-size: ${typographyRole.label.fontSize}; + font-weight: 700; + color: #ffffff; + text-decoration: none; + transition: all 0.3s ease; + + &:hover { + transform: translateY(-2px); + background: linear-gradient(to right, ${palette.juhong[600]}, ${palette.juhong[800]}); + } + + @media (min-width: 640px) { + width: auto; + } + + & .arrow { + display: inline-block; + transition: transform 0.3s ease; + } + + &:hover .arrow { + transform: translateX(4px); + } +`; + +const SecondaryCtaLink = styled(Link)` + display: inline-flex; + width: 100%; + align-items: center; + justify-content: center; + border-radius: 9999px; + background: #ffffff; + padding: 0.875rem 1.75rem; + font-size: ${typographyRole.label.fontSize}; + font-weight: 700; + color: ${palette.juhong[500]}; + text-decoration: none; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); + transition: all 0.3s ease; + + &:hover { + transform: translateY(-2px); + background: ${palette.juhong[50]}; + } + + [data-theme='dark'] & { + background: ${surface.dark.surface}; + color: ${meok[100]}; + border: 1px solid rgba(255, 255, 255, 0.1); + + &:hover { + background: ${surface.dark.elevated}; + color: #ffffff; + } + } + + @media (min-width: 640px) { + width: auto; + } +`; + +const SubText = styled.p` + margin-top: 2rem; + font-size: ${typographyRole.caption.fontSize}; + font-weight: 500; + color: ${meok[500]}; + + [data-theme='dark'] & { + color: ${meok[400]}; + } +`; + +export const SorimaruFooterCTA: React.FC = () => { + const term = useMemo(() => getNextSolarTerm(), []); + + const termText = (() => { + if (!term) return '절기 알림'; + if (term.daysLeft === 0) return `${term.name} 오늘`; + if (term.daysLeft === 1) return `${term.name} 내일`; + return `${term.name}까지 D-${term.daysLeft}`; + })(); + + return ( + + + + {} + + + + + + {} + + + {termText} + + + + 다음 계절에도, 새로운 이야기를 만나요 + + + + 계절과 날짜가 바뀌면 오늘의 대표 이야기도 새롭게 열려요. + 다음에 돌아왔을 때 다른 장소의 온기를 이어서 들어보세요. + + + {} + + + 전국 한옥 지도에서 둘러보기 + → + + + 온마루 3D 한옥 이야기 보러 가기 + + + + 오늘의 소리는 내일 또 다른 장면으로 이어져요. + + + + + ); +}; diff --git a/src/features/sorimaru-audio/components/SorimaruQuestionAssistant.tsx b/src/features/sorimaru-audio/components/SorimaruQuestionAssistant.tsx index 3066ce06..ea85c102 100644 --- a/src/features/sorimaru-audio/components/SorimaruQuestionAssistant.tsx +++ b/src/features/sorimaru-audio/components/SorimaruQuestionAssistant.tsx @@ -6,7 +6,7 @@ import { keyframes } from '@emotion/react'; import { HugeiconsIcon } from '@hugeicons/react' import { ArrowUp01Icon, RotateCcwIcon, Search01Icon } from '@hugeicons/core-free-icons' import type { SorimaruAssistantFilters, SorimaruAssistantResponse, SorimaruAssistantSource } from '@/features/sorimaru-audio/api/sorimaruAssistant.types'; -import { palette, meok, surface, fontSize } from '@/design-system/tokens'; +import { palette, meok, surface, typographyRole } from '@/design-system/tokens'; interface SorimaruQuestionAssistantProps { filters: SorimaruAssistantFilters; @@ -48,7 +48,7 @@ const CardBox = styled.div` const QuestionHeading = styled.h3` font-family: inherit; - font-size: ${fontSize.lg}; + font-size: ${typographyRole.cardTitle.fontSize}; font-weight: 700; letter-spacing: -0.035em; color: ${meok[900]}; @@ -60,7 +60,7 @@ const QuestionHeading = styled.h3` const QuestionDesc = styled.p` margin-top: 0.375rem; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; line-height: 1.25rem; color: ${meok[700]}; @@ -82,7 +82,7 @@ const SuggestionButton = styled.button` background-color: #ffffff; padding: 0.375rem 0.625rem; text-align: left; - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; line-height: 1rem; color: ${meok[700]}; cursor: pointer; @@ -140,7 +140,7 @@ const StyledInput = styled.input` background: transparent; border: none; font-family: inherit; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; color: ${meok[900]}; outline: none; @@ -182,7 +182,7 @@ const SubmitButton = styled.button` const HelperText = styled.p` margin-top: 0.75rem; - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; color: ${meok[700]}; line-height: 1.25rem; @@ -208,7 +208,7 @@ const ResultCard = styled.div` `; const ResultAnswer = styled.p` - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; line-height: 1.5rem; color: ${meok[900]}; @@ -258,7 +258,7 @@ const SourceButton = styled.button` overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 600; color: ${meok[900]}; @@ -273,7 +273,7 @@ const SourceButton = styled.button` overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; color: ${meok[700]}; [data-theme='dark'] & { diff --git a/src/features/sorimaru-audio/components/StoryCarousel.tsx b/src/features/sorimaru-audio/components/StoryCarousel.tsx index 37fe9916..23ec83ff 100644 --- a/src/features/sorimaru-audio/components/StoryCarousel.tsx +++ b/src/features/sorimaru-audio/components/StoryCarousel.tsx @@ -1,4 +1,4 @@ -'use client'; +'use client'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import styled from '@emotion/styled'; @@ -11,7 +11,7 @@ import { ChevronLeftIcon, ChevronRightIcon, MapIcon, PauseIcon, PlayIcon } from import { useSorimaruAudioStore } from '@/features/sorimaru-audio/store/useSorimaruAudioStore'; import type { SorimaruStorySummary } from '@/features/sorimaru-audio/domain/sorimaruStory'; import { getRailIndicator, shouldUpdateRailIndicator } from './storyCarouselMetrics'; -import { palette, meok, surface, fontSize } from '@/design-system/tokens'; +import { palette, meok, surface, typographyRole } from '@/design-system/tokens'; import { OniSearchEmpty } from '@/shared/components/OniSearchEmpty/OniSearchEmpty'; interface StoryCarouselProps { @@ -210,7 +210,7 @@ const CardInfoCol = styled.div` const CardMainTitle = styled.h3<{ $isCurrent: boolean }>` font-family: var(--font-hanok); - font-size: ${fontSize.base}; + font-size: ${typographyRole.body.fontSize}; font-weight: 700; line-height: 1.35; letter-spacing: -0.035em; @@ -226,7 +226,7 @@ const CardMainTitle = styled.h3<{ $isCurrent: boolean }>` } @media (min-width: 640px) { - font-size: ${fontSize.lg}; + font-size: ${typographyRole.cardTitle.fontSize}; } `; @@ -235,7 +235,7 @@ const CardSubTitle = styled.p` overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 500; line-height: 1rem; color: ${meok[700]}; @@ -258,7 +258,7 @@ const MiniCategoryTag = styled.span` border-radius: 4px; background-color: ${palette.juhong[50]}; padding: 1px 0.375rem; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 600; line-height: 1rem; color: ${palette.juhong[600]}; @@ -273,7 +273,7 @@ const LocationSpan = styled.span` overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 500; line-height: 1rem; color: ${meok[500]}; @@ -289,7 +289,7 @@ const ExcerptText = styled.p` -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; line-height: 1.4; color: ${meok[500]}; @@ -304,7 +304,7 @@ const CardBottomMeta = styled.div` align-items: center; justify-content: space-between; padding-top: 0.5rem; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; color: ${meok[500]}; [data-theme='dark'] & { @@ -602,7 +602,7 @@ const CounterContainer = styled.div` align-items: center; justify-content: flex-end; padding: 0.25rem 1.5rem 0.75rem; - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; font-weight: 500; letter-spacing: 0.08em; color: ${meok[500]}; diff --git a/src/features/sorimaru-audio/presentation/SorimaruRequestErrorState.tsx b/src/features/sorimaru-audio/presentation/SorimaruRequestErrorState.tsx index 444f2edc..793fe0fc 100644 --- a/src/features/sorimaru-audio/presentation/SorimaruRequestErrorState.tsx +++ b/src/features/sorimaru-audio/presentation/SorimaruRequestErrorState.tsx @@ -5,7 +5,7 @@ import styled from '@emotion/styled'; import { HugeiconsIcon } from '@hugeicons/react'; import { RotateCcwIcon } from '@hugeicons/core-free-icons'; import { OniSearchEmpty } from '@/shared/components/OniSearchEmpty/OniSearchEmpty'; -import { fontSize, meok, palette } from '@/design-system/tokens'; +import { typographyRole, meok, palette } from '@/design-system/tokens'; const RetryButton = styled.button` display: inline-flex; @@ -19,7 +19,7 @@ const RetryButton = styled.button` padding: 0.5rem 1rem; color: ${meok[800]}; font-family: inherit; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 700; cursor: pointer; transition: border-color 0.18s ease, color 0.18s ease, transform 0.18s ease; diff --git a/src/private/core-ui/map-warmth/DateScrubber.tsx b/src/private/core-ui/map-warmth/DateScrubber.tsx index 6218f89d..02f9c61d 100644 --- a/src/private/core-ui/map-warmth/DateScrubber.tsx +++ b/src/private/core-ui/map-warmth/DateScrubber.tsx @@ -4,7 +4,7 @@ import { useEffect, useMemo, useRef, useState } from 'react'; import styled from '@emotion/styled'; import { HugeiconsIcon } from '@hugeicons/react' import { PauseIcon, PlayIcon } from '@hugeicons/core-free-icons' -import { lightPalette, meok, surface , fontSize } from '@/design-system/tokens'; +import { lightPalette, meok, surface, typographyRole } from '@/design-system/tokens'; import { useMapStore } from '@/features/map/hooks/useMapStore'; import { SNAP_CSS } from '@/features/map/components/BottomSheet'; import type { SheetSnap } from '@/features/map/types'; @@ -143,7 +143,7 @@ const PlayButton = styled.button<{ $playing: boolean }>` const Stamp = styled.p` margin: 0; - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; font-weight: 500; color: ${meok[900]}; font-variant-numeric: tabular-nums; @@ -155,7 +155,7 @@ const Stamp = styled.p` span { margin-left: 6px; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 500; color: ${meok[500]}; } @@ -163,7 +163,7 @@ const Stamp = styled.p` const Verdict = styled.p<{ $tone: 'quiet' | 'busy' | 'flat' }>` margin: 0; - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; font-weight: 500; font-variant-numeric: tabular-nums; white-space: nowrap; @@ -209,7 +209,7 @@ const Bar = styled.div<{ $live: boolean }>` const Ends = styled.div` display: flex; justify-content: space-between; - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; font-weight: 500; color: ${meok[500]}; font-variant-numeric: tabular-nums; diff --git a/src/private/core-ui/map-warmth/MoodSelector.tsx b/src/private/core-ui/map-warmth/MoodSelector.tsx index e6f9242e..779507f6 100644 --- a/src/private/core-ui/map-warmth/MoodSelector.tsx +++ b/src/private/core-ui/map-warmth/MoodSelector.tsx @@ -4,7 +4,7 @@ import React from 'react'; import styled from '@emotion/styled'; import { HugeiconsIcon } from '@hugeicons/react' import { SmilePlusIcon, SmileIcon, MehIcon, FrownIcon, AngryIcon } from '@hugeicons/core-free-icons' -import { lightPalette, meok , fontSize } from '@/design-system/tokens'; +import { lightPalette, meok, typographyRole } from '@/design-system/tokens'; export type MoodValue = 1 | 2 | 3 | 4 | 5; @@ -64,7 +64,7 @@ const FaceButton = styled.button<{ $selected: boolean; $readonly: boolean }>` `; const MoodLabel = styled.span` - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; font-weight: 500; color: ${meok[900]}; white-space: nowrap; diff --git a/src/private/core-ui/map-warmth/PlaceWarmthSection.tsx b/src/private/core-ui/map-warmth/PlaceWarmthSection.tsx index 24140a4a..ea9b4abc 100644 --- a/src/private/core-ui/map-warmth/PlaceWarmthSection.tsx +++ b/src/private/core-ui/map-warmth/PlaceWarmthSection.tsx @@ -4,7 +4,7 @@ import React, { useState } from 'react'; import styled from '@emotion/styled'; import { HugeiconsIcon } from '@hugeicons/react' import { FlameIcon, Leaf01Icon, UsersIcon, PlusSignIcon, MessageCircleIcon } from '@hugeicons/core-free-icons' -import { lightPalette, palette, meok, surface, fontSize } from '@/design-system/tokens'; +import { palette, meok, surface, typographyRole } from '@/design-system/tokens'; import { usePlaceVisitReviews } from '@/features/visit-review/presentation/usePlaceVisitReviews'; import WriteWarmthModal from './WriteWarmthModal'; import MoodSelector from './MoodSelector'; @@ -37,7 +37,7 @@ const TitleBox = styled.div` const SectionTitle = styled.h3` margin: 0; - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; font-weight: 500; color: ${meok[900]}; @@ -56,7 +56,7 @@ const CountBadge = styled.span` border-radius: 9999px; background: ${palette.juhong[50]}; color: ${palette.juhong[700]}; - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; font-weight: 700; font-variant-numeric: tabular-nums; border: none; @@ -78,7 +78,7 @@ const WriteButton = styled.button` background: ${palette.juhong[50]}; color: ${palette.juhong[700]}; font-family: inherit; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 600; cursor: pointer; transition: all 0.15s ease; @@ -144,7 +144,7 @@ const MoodBadge = styled.span<{ $mood?: '한적' | '북적' }>` gap: 4px; padding: 2.5px 7px; border-radius: 6px; - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; font-weight: 700; color: ${palette.juhong[800]}; background: ${palette.juhong[50]}; @@ -166,12 +166,12 @@ const MineBadge = styled.span` border-radius: 4px; background: ${meok[900]}; color: #ffffff; - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; font-weight: 700; `; const TimeText = styled.span` - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; color: ${meok[400]}; `; @@ -191,7 +191,7 @@ const TagItem = styled.span` border-radius: 6px; background: rgba(255, 106, 16, 0.08); color: ${palette.juhong[800]}; - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; font-weight: 500; [data-theme='dark'] & { @@ -202,7 +202,7 @@ const TagItem = styled.span` const WarmthText = styled.p` margin: 0; - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; font-weight: 500; line-height: 1.55; color: ${meok[900]}; @@ -246,7 +246,7 @@ const EmptyIconBox = styled.div` `; const EmptyTitle = styled.div` - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; font-weight: 500; color: ${meok[900]}; margin-bottom: 4px; @@ -257,7 +257,7 @@ const EmptyTitle = styled.div` `; const EmptySub = styled.div` - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; color: ${meok[500]}; margin-bottom: 12px; @@ -278,7 +278,7 @@ const EmptyActionBtn = styled.button` background: ${palette.juhong[500]}; color: #ffffff; font-family: inherit; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 500; cursor: pointer; diff --git a/src/private/core-ui/map-warmth/PopularPlacesPanel.tsx b/src/private/core-ui/map-warmth/PopularPlacesPanel.tsx index e1144915..e8d38bc9 100644 --- a/src/private/core-ui/map-warmth/PopularPlacesPanel.tsx +++ b/src/private/core-ui/map-warmth/PopularPlacesPanel.tsx @@ -1,10 +1,10 @@ -'use client'; +'use client'; import React, { useState, useEffect } from 'react'; import styled from '@emotion/styled'; import { HugeiconsIcon } from '@hugeicons/react' import { Cancel01Icon, LandmarkIcon } from '@hugeicons/core-free-icons' -import { lightPalette, meok, surface, fontSize } from '@/design-system/tokens'; +import { lightPalette, meok, surface, typographyRole } from '@/design-system/tokens'; import { useMapStore } from '@/features/map/hooks/useMapStore'; import type { RankedPlace } from '@/features/map/types'; @@ -40,7 +40,7 @@ const TopBar = styled.div` const Title = styled.h3` margin: 0; - font-size: ${fontSize.base}; + font-size: ${typographyRole.body.fontSize}; font-weight: 500; color: ${meok[900]}; letter-spacing: -0.02em; @@ -86,7 +86,7 @@ const SubHeader = styled.div` const SubText = styled.p` margin: 0 0 12px; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; color: ${meok[500]}; [data-theme='dark'] & { @@ -115,7 +115,7 @@ const RegionChip = styled.button<{ $active: boolean }>` $active ? meok[900] : 'rgba(78, 89, 104, 0.07)'}; color: ${({ $active }) => ($active ? '#ffffff' : meok[700])}; font-family: inherit; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 500; cursor: pointer; white-space: nowrap; @@ -185,7 +185,7 @@ const LeftCol = styled.div` `; const RankNum = styled.span<{ $rank: number }>` - font-size: ${fontSize.base}; + font-size: ${typographyRole.body.fontSize}; font-weight: 700; color: ${({ $rank }) => $rank === 1 @@ -207,7 +207,7 @@ const PlaceInfo = styled.div` const Name = styled.h4` margin: 0 0 4px; - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; font-weight: 500; color: ${meok[900]}; overflow: hidden; @@ -221,7 +221,7 @@ const Name = styled.h4` const Meta = styled.p` margin: 0; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; color: ${meok[500]}; display: flex; align-items: center; diff --git a/src/private/core-ui/map-warmth/TagGroup.tsx b/src/private/core-ui/map-warmth/TagGroup.tsx index 1259fa52..a41cec3c 100644 --- a/src/private/core-ui/map-warmth/TagGroup.tsx +++ b/src/private/core-ui/map-warmth/TagGroup.tsx @@ -1,6 +1,6 @@ import React from 'react'; import styled from '@emotion/styled'; -import { meok , fontSize } from '@/design-system/tokens'; +import { meok, typographyRole } from '@/design-system/tokens'; interface TagGroupProps { tags: string[]; @@ -15,7 +15,7 @@ const Wrapper = styled.div` const TagBadge = styled.span` display: inline-flex; align-items: center; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 500; padding: 3px 8.5px; border-radius: 9999px; diff --git a/src/private/core-ui/map-warmth/WarmthCard.tsx b/src/private/core-ui/map-warmth/WarmthCard.tsx index 834e8b65..1d37e4e5 100644 --- a/src/private/core-ui/map-warmth/WarmthCard.tsx +++ b/src/private/core-ui/map-warmth/WarmthCard.tsx @@ -1,10 +1,10 @@ -'use client'; +'use client'; import React, { useState } from 'react'; import styled from '@emotion/styled'; import { HugeiconsIcon } from '@hugeicons/react' import { LandmarkIcon, Home01Icon, UtensilsIcon, Coffee01Icon, ShoppingBag01Icon, FlameIcon, Leaf01Icon, ChevronRightIcon, ChevronUpIcon, ChevronDownIcon } from '@hugeicons/core-free-icons' -import { lightPalette, darkPalette, meok, surface , fontSize } from '@/design-system/tokens'; +import { lightPalette, darkPalette, meok, surface, typographyRole } from '@/design-system/tokens'; import { OniAvatar } from '@/features/profile/OniAvatar'; import { useMapStore } from '@/features/map/hooks/useMapStore'; import { toggleHelpful } from '@/features/map/warmth/warmthRepo'; @@ -80,7 +80,7 @@ const PlaceTitleRow = styled.div` const PlaceName = styled.h4` margin: 0; - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; font-weight: 500; color: ${meok[900]}; overflow: hidden; @@ -98,7 +98,7 @@ const CrowdMoodBadge = styled.span<{ $crowd?: '북적' | '한적' }>` gap: 3px; padding: 2px 7px; border-radius: 9999px; - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; font-weight: 700; white-space: nowrap; flex-shrink: 0; @@ -125,7 +125,7 @@ const MascotBadgeIcon = styled.img` const PlaceMeta = styled.p` margin: 0; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; color: ${meok[500]}; `; @@ -145,7 +145,7 @@ const TagSection = styled.div` const ReviewText = styled.p<{ $expanded: boolean }>` margin: 0; - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; line-height: 1.6; color: ${meok[900]}; word-break: keep-all; @@ -171,7 +171,7 @@ const TextToggleBtn = styled.button` border: none; background: transparent; color: ${meok[700]}; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 500; cursor: pointer; @@ -190,7 +190,7 @@ const FooterMeta = styled.div` `; const MetaDate = styled.span` - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; color: ${meok[500]}; `; @@ -207,7 +207,7 @@ const HelpfulButton = styled.button<{ $active: boolean }>` color: ${({ $active }) => $active ? lightPalette.hwanggeum[700] : meok[700]}; font-family: inherit; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 500; cursor: pointer; transition: all 0.15s ease; @@ -286,7 +286,7 @@ const RelatedInfo = styled.div` const RelatedName = styled.h5` margin: 0; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 500; color: ${meok[900]}; overflow: hidden; @@ -295,7 +295,7 @@ const RelatedName = styled.h5` `; const RelatedMeta = styled.span` - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; color: ${meok[500]}; `; diff --git a/src/private/core-ui/map-warmth/WarmthFeed.styles.ts b/src/private/core-ui/map-warmth/WarmthFeed.styles.ts index 532c3e01..bd22cc67 100644 --- a/src/private/core-ui/map-warmth/WarmthFeed.styles.ts +++ b/src/private/core-ui/map-warmth/WarmthFeed.styles.ts @@ -1,8 +1,8 @@ -import styled from '@emotion/styled'; +import styled from '@emotion/styled'; import { keyframes } from '@emotion/react'; import { HugeiconsIcon } from '@hugeicons/react' import { ChevronDownIcon } from '@hugeicons/core-free-icons' -import { lightPalette, meok, surface, fontSize } from '@/design-system/tokens'; +import { lightPalette, meok, surface, typographyRole } from '@/design-system/tokens'; export const FeedContainer = styled.div` display: flex; @@ -42,7 +42,7 @@ export const SectionTitleGroup = styled.div` export const SectionTitle = styled.h3` margin: 0; - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; font-weight: 500; color: ${meok[900]}; letter-spacing: -0.02em; @@ -128,7 +128,7 @@ export const RegionChip = styled.button<{ $active: boolean }>` background: ${({ $active }) => ($active ? '#191f28' : 'rgba(0,0,0,0.05)')}; color: ${({ $active }) => ($active ? '#ffffff' : '#333d4b')}; font-family: inherit; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: ${({ $active }) => ($active ? 600 : 400)}; cursor: pointer; white-space: nowrap; @@ -226,7 +226,7 @@ export const FeaturedInfo = styled.div` `; export const FeaturedRank = styled.span` - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; font-weight: 700; color: ${lightPalette.hwanggeum[700]}; display: block; @@ -250,7 +250,7 @@ export const FeaturedName = styled.h4` export const FeaturedMeta = styled.p` margin: 0; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; color: ${meok[500]}; `; @@ -264,7 +264,7 @@ export const MoreBtn = styled.button` background: #ffffff; color: ${meok[900]}; font-family: inherit; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 500; cursor: pointer; flex-shrink: 0; @@ -295,7 +295,7 @@ export const ReviewSectionHeader = styled.div` export const ReviewSectionTitle = styled.h4` margin: 0; - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; font-weight: 500; color: ${meok[900]}; display: flex; @@ -340,7 +340,7 @@ export const SortSelect = styled.select` box-shadow: none !important; font-family: inherit; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 500; color: ${meok[700]}; padding: 4px 24px 4px 10px; @@ -384,7 +384,7 @@ export const EmptyState = styled.div` padding: 40px 16px; text-align: center; color: ${meok[500]}; - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; line-height: 1.6; `; @@ -408,7 +408,7 @@ export const PageNavBtn = styled.button` background: rgba(78, 89, 104, 0.06); color: ${meok[700]}; font-family: inherit; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 500; cursor: pointer; transition: all 0.15s ease; @@ -444,7 +444,7 @@ export const PageNumberBtn = styled.button<{ $active: boolean }>` $active ? lightPalette.hwanggeum[500] : 'transparent'}; color: ${({ $active }) => ($active ? '#ffffff' : meok[700])}; font-family: inherit; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: ${({ $active }) => ($active ? 700 : 500)}; cursor: pointer; transition: all 0.15s ease; @@ -457,7 +457,7 @@ export const PageNumberBtn = styled.button<{ $active: boolean }>` `; export const PageIndicator = styled.span` - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 500; color: ${meok[500]}; margin-left: 4px; @@ -516,7 +516,7 @@ export const OniBannerTextCol = styled.div` export const OniBannerTitle = styled.h4` margin: 0; - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; font-weight: 700; color: ${meok[900]}; letter-spacing: -0.02em; @@ -537,7 +537,7 @@ export const OniBannerBadge = styled.span` display: inline-flex; align-items: center; gap: 4px; - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; font-weight: 600; color: ${lightPalette.hwanggeum[700]}; background: rgba(245, 166, 35, 0.12); @@ -552,7 +552,7 @@ export const OniBannerBadge = styled.span` export const OniBannerDesc = styled.p` margin: 2px 0 0; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; color: ${meok[600]}; line-height: 1.4; letter-spacing: -0.01em; diff --git a/src/private/core-ui/map-warmth/WarmthLayer.tsx b/src/private/core-ui/map-warmth/WarmthLayer.tsx index 6cc05cb1..220fa846 100644 --- a/src/private/core-ui/map-warmth/WarmthLayer.tsx +++ b/src/private/core-ui/map-warmth/WarmthLayer.tsx @@ -10,7 +10,7 @@ import { darkPalette, meok, surface, - fontSize, } from '@/design-system/tokens'; + typographyRole, } from '@/design-system/tokens'; import { useOnmaruTheme } from '@/design-system/ThemeProvider'; import { paintOverlays, type OverlaySpec } from '@/features/map/hooks/overlay'; import { useMapStore } from '@/features/map/hooks/useMapStore'; @@ -182,7 +182,7 @@ const styles = css` } .om-surge-pill-name { - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 700; letter-spacing: -0.2px; white-space: nowrap; @@ -193,7 +193,7 @@ const styles = css` .om-surge-pill-delta { - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 700; font-variant-numeric: tabular-nums; white-space: nowrap; @@ -320,7 +320,7 @@ const styles = css` } .om-popover-title { - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; font-weight: 500; color: ${meok[900]}; white-space: nowrap; @@ -338,7 +338,7 @@ const styles = css` gap: 4px; padding: 3px 8px; border-radius: 9999px; - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; font-weight: 700; white-space: nowrap; } @@ -364,7 +364,7 @@ const styles = css` justify-content: space-between; gap: 10px; margin: 0; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; } .om-now-day { @@ -451,7 +451,7 @@ const styles = css` } .om-week-label { - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; font-weight: 500; color: ${meok[500]}; line-height: 1; @@ -468,7 +468,7 @@ const styles = css` .om-week-say { margin: 0; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 500; color: ${meok[700]}; } @@ -490,7 +490,7 @@ const styles = css` display: flex; align-items: center; gap: 12px; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; color: ${meok[500]}; } @@ -506,7 +506,7 @@ const styles = css` padding: 6px 8px; border-radius: 8px; background: rgba(120, 120, 120, 0.08); - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; color: ${lightPalette.hwanggeum[900]}; font-weight: 500; text-align: center; diff --git a/src/private/core-ui/map-warmth/WarmthLegend.tsx b/src/private/core-ui/map-warmth/WarmthLegend.tsx index fd9cf7a7..700c70d5 100644 --- a/src/private/core-ui/map-warmth/WarmthLegend.tsx +++ b/src/private/core-ui/map-warmth/WarmthLegend.tsx @@ -1,10 +1,10 @@ -'use client'; +'use client'; import { useMemo } from 'react'; import styled from '@emotion/styled'; import { HugeiconsIcon } from '@hugeicons/react' import { MapPinIcon, FlameIcon } from '@hugeicons/core-free-icons' -import { meok, surface , fontSize } from '@/design-system/tokens'; +import { meok, typographyRole } from '@/design-system/tokens'; import { useOnmaruTheme } from '@/design-system/ThemeProvider'; import { useMapStore } from '@/features/map/hooks/useMapStore'; import { @@ -57,7 +57,7 @@ const Root = styled.div<{ $isDark: boolean }>` const Summary = styled.p<{ $isDark: boolean }>` margin: 0; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 500; line-height: 1.35; color: ${({ $isDark }) => ($isDark ? '#F3F4F6' : meok[900])}; @@ -99,7 +99,7 @@ const PeriodRow = styled.div` const PeriodLabel = styled.span<{ $isDark: boolean }>` margin-right: 2px; - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; font-weight: 500; color: ${({ $isDark }) => ($isDark ? '#9CA3AF' : meok[500])}; @@ -130,7 +130,7 @@ const PeriodBtn = styled.button<{ $active: boolean; $isDark: boolean }>` ? '#D1D5DB' : meok[700]}; font-family: inherit; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: ${({ $active }) => ($active ? 700 : 500)}; cursor: pointer; transition: all 0.15s ease; @@ -186,7 +186,7 @@ const ViewTypeBtn = styled.button<{ $active: boolean; $isDark: boolean }>` border-radius: 9px; border: none; font-family: inherit; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 600; cursor: pointer; transition: all 0.18s cubic-bezier(0.16, 1, 0.3, 1); diff --git a/src/private/core-ui/map-warmth/WarmthNotesLayer.tsx b/src/private/core-ui/map-warmth/WarmthNotesLayer.tsx index cb74171a..88fa189c 100644 --- a/src/private/core-ui/map-warmth/WarmthNotesLayer.tsx +++ b/src/private/core-ui/map-warmth/WarmthNotesLayer.tsx @@ -2,7 +2,7 @@ import { useEffect, useMemo } from 'react'; import { Global, css } from '@emotion/react'; -import { meok, lightPalette, darkPalette, surface , fontSize } from '@/design-system/tokens'; +import { meok, lightPalette, darkPalette, surface, typographyRole } from '@/design-system/tokens'; import { useOnmaruTheme } from '@/design-system/ThemeProvider'; import { useMapStore } from '@/features/map/hooks/useMapStore'; import { paintOverlays, type OverlaySpec } from '@/features/map/hooks/overlay'; @@ -114,7 +114,7 @@ const styles = css` } .om-note-pin-place { - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 500; color: ${meok[900]}; } @@ -129,7 +129,7 @@ const styles = css` border-radius: 9999px; background: ${meok[900]}; color: #ffffff; - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; font-weight: 700; line-height: 15px; text-align: center; @@ -189,7 +189,7 @@ const styles = css` } .om-note-place { - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 700; color: ${meok[900]}; white-space: nowrap; @@ -261,7 +261,7 @@ const styles = css` } .om-note-page-indicator { - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; font-weight: 700; color: ${meok[500]}; font-variant-numeric: tabular-nums; @@ -273,7 +273,7 @@ const styles = css` gap: 2.5px; padding: 1.5px 5.5px; border-radius: 9999px; - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; font-weight: 700; white-space: nowrap; } @@ -297,7 +297,7 @@ const styles = css` .om-note-body { flex: 1; padding: 0 8px; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; line-height: 1.45; font-weight: 500; color: ${meok[900]}; @@ -320,7 +320,7 @@ const styles = css` align-items: center; justify-content: space-between; margin-top: 4px; - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; color: ${meok[500]}; } @@ -328,7 +328,7 @@ const styles = css` border: none; background: transparent; padding: 2px 6px; - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; font-weight: 700; color: ${lightPalette.hwanggeum[500]}; cursor: pointer; diff --git a/src/private/core-ui/map-warmth/WriteButton.tsx b/src/private/core-ui/map-warmth/WriteButton.tsx index eae23ed3..87827884 100644 --- a/src/private/core-ui/map-warmth/WriteButton.tsx +++ b/src/private/core-ui/map-warmth/WriteButton.tsx @@ -4,7 +4,7 @@ import React, { useState } from 'react'; import styled from '@emotion/styled'; import { HugeiconsIcon } from '@hugeicons/react' import { PenLineIcon } from '@hugeicons/core-free-icons' -import { meok, fontSize } from '@/design-system/tokens'; +import { meok, typographyRole } from '@/design-system/tokens'; import { useMapStore } from '@/features/map/hooks/useMapStore'; import { hasAuthenticatedUser, showLoginRequiredToast } from '@/features/auth/privateState'; import WriteWarmthModal from './WriteWarmthModal'; @@ -22,7 +22,7 @@ const FloatingBtn = styled.button` background: ${meok[900]}; color: #ffffff; font-family: inherit; - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; font-weight: 700; cursor: pointer; @@ -49,7 +49,7 @@ const FloatingBtn = styled.button` @media (max-width: 1023px) { height: 38px; padding: 0 14px; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; } `; diff --git a/src/private/core-ui/sorimaru/LocalMiniPlayer.tsx b/src/private/core-ui/sorimaru/LocalMiniPlayer.tsx index 0af238ee..be4c02c5 100644 --- a/src/private/core-ui/sorimaru/LocalMiniPlayer.tsx +++ b/src/private/core-ui/sorimaru/LocalMiniPlayer.tsx @@ -5,7 +5,7 @@ import styled from '@emotion/styled'; import { AnimatePresence, motion } from 'framer-motion'; import { HugeiconsIcon } from '@hugeicons/react' import { PlayIcon, PauseIcon, Cancel01Icon, HeartIcon, RotateCcwIcon, RotateCwIcon, Compass01Icon, BookOpen01Icon } from '@hugeicons/core-free-icons' -import { meok, palette, surface, fontSize } from '@/design-system/tokens'; +import { meok, palette, surface, typographyRole } from '@/design-system/tokens'; import { useSorimaruAudioStore } from '@/features/sorimaru-audio/store/useSorimaruAudioStore'; import { useSorimaruAudioPlayer } from '@/features/sorimaru-audio/hooks/useSorimaruAudioPlayer'; import { useSorimaruImage } from '@/features/sorimaru-audio/hooks/useSorimaruImage'; @@ -231,7 +231,7 @@ const StoryTitle = styled.span` text-overflow: ellipsis; white-space: nowrap; font-family: var(--font-hanok); - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; font-weight: 600; color: ${meok[900]}; @@ -243,7 +243,7 @@ const StoryTitle = styled.span` const StorySubMeta = styled.span` display: flex; align-items: center; - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; color: ${meok[500]}; margin-top: 0.125rem; diff --git a/src/private/core-ui/sorimaru/SorimaruEditorialRail.tsx b/src/private/core-ui/sorimaru/SorimaruEditorialRail.tsx index 3ff78ad3..ee0705ab 100644 --- a/src/private/core-ui/sorimaru/SorimaruEditorialRail.tsx +++ b/src/private/core-ui/sorimaru/SorimaruEditorialRail.tsx @@ -5,23 +5,23 @@ import styled from '@emotion/styled'; import { keyframes } from '@emotion/react'; import { motion } from 'framer-motion'; import gsap from 'gsap'; -import { HugeiconsIcon } from '@hugeicons/react'; -import { ChevronLeftIcon, ChevronRightIcon } from '@hugeicons/core-free-icons'; +import { HugeiconsIcon } from '@hugeicons/react'; +import { ChevronLeftIcon, ChevronRightIcon } from '@hugeicons/core-free-icons'; import type { SorimaruStorySummary } from '@/features/sorimaru-audio/domain/sorimaruStory'; import { editorialRailCardViewModel } from '@/features/sorimaru-audio/presentation/sorimaruEditorialRailViewModel'; import { SORIMARU_THEME_CATEGORIES } from '@/features/sorimaru-audio/data/sorimaruCategoryData'; import { SORIMARU_RAIL_VISIBLE_BUFFER, getVisibleRailPositions } from './sorimaruEditorialRailModel'; -import { palette, meok, surface, fontSize } from '@/design-system/tokens'; +import { palette, meok, surface, typographyRole } from '@/design-system/tokens'; import { OniSearchEmpty } from '@/shared/components/OniSearchEmpty/OniSearchEmpty'; -import { SorimaruRequestErrorState } from '@/features/sorimaru-audio/presentation/SorimaruRequestErrorState'; +import { SorimaruRequestErrorState } from '@/features/sorimaru-audio/presentation/SorimaruRequestErrorState'; interface SorimaruEditorialRailProps { stories: SorimaruStorySummary[]; - storySets?: Record; - isActive?: boolean; + storySets?: Record; + isActive?: boolean; isLoading?: boolean; - error?: Error | null; - onRetry?: () => void; + error?: Error | null; + onRetry?: () => void; onSelectStory: (story: SorimaruStorySummary, intent: 'play') => void; } @@ -85,14 +85,14 @@ const isTrustedSorimaruImage = (imageUrl: string) => !imageUrl.includes('unsplash.com') && !imageUrl.includes('pixabay.com'); -const CardMotionButton = styled(motion.button)<{ $isActive: boolean }>` +const CardMotionButton = styled(motion.button)<{ $isActive: boolean }>` position: relative; height: 280px; width: 175px; flex-shrink: 0; user-select: none; - overflow: visible; - border-radius: 0; + overflow: visible; + border-radius: 0; isolation: isolate; background-color: transparent; text-align: left; @@ -100,13 +100,13 @@ const CardMotionButton = styled(motion.button)<{ $isActive: boolean }>` border: none; cursor: pointer; z-index: ${({ $isActive }) => ($isActive ? 20 : 10)}; - filter: ${({ $isActive }) => ($isActive ? 'none' : 'grayscale(0.12)')}; - transition: filter 0.3s ease; - - [data-theme='dark'] & { - background-color: transparent; - box-shadow: none; - } + filter: ${({ $isActive }) => ($isActive ? 'none' : 'grayscale(0.12)')}; + transition: filter 0.3s ease; + + [data-theme='dark'] & { + background-color: transparent; + box-shadow: none; + } &:hover { filter: grayscale(0); @@ -120,46 +120,46 @@ const CardMotionButton = styled(motion.button)<{ $isActive: boolean }>` height: 380px; width: 250px; } -`; - -const CardSurface = styled.div<{ $isActive: boolean }>` - position: absolute; - inset: 0; - overflow: hidden; - border-radius: 1.25rem; - background-color: transparent; - box-shadow: ${({ $isActive }) => - $isActive ? '0 12px 28px rgba(0, 0, 0, 0.12)' : '0 4px 14px rgba(0, 0, 0, 0.04)'}; - transition: box-shadow 0.3s ease; - - [data-theme='dark'] & { - background-color: ${surface.dark.card}; - box-shadow: ${({ $isActive }) => - $isActive ? '0 14px 32px rgba(0, 0, 0, 0.45)' : '0 4px 14px rgba(0, 0, 0, 0.25)'}; - } -`; - -const CardEdgeMask = styled.div` - display: none; - - [data-theme='dark'] & { - position: absolute; - inset: -1px; - z-index: 30; - display: block; - box-sizing: border-box; - border: 3px solid ${surface.dark.app}; - border-radius: 21px; - pointer-events: none; - } -`; - -const CardImageLayer = styled(motion.div)` - position: absolute; - inset: 0; -`; - -const skeletonShimmer = keyframes` +`; + +const CardSurface = styled.div<{ $isActive: boolean }>` + position: absolute; + inset: 0; + overflow: hidden; + border-radius: 1.25rem; + background-color: transparent; + box-shadow: ${({ $isActive }) => + $isActive ? '0 12px 28px rgba(0, 0, 0, 0.12)' : '0 4px 14px rgba(0, 0, 0, 0.04)'}; + transition: box-shadow 0.3s ease; + + [data-theme='dark'] & { + background-color: ${surface.dark.card}; + box-shadow: ${({ $isActive }) => + $isActive ? '0 14px 32px rgba(0, 0, 0, 0.45)' : '0 4px 14px rgba(0, 0, 0, 0.25)'}; + } +`; + +const CardEdgeMask = styled.div` + display: none; + + [data-theme='dark'] & { + position: absolute; + inset: -1px; + z-index: 30; + display: block; + box-sizing: border-box; + border: 3px solid ${surface.dark.app}; + border-radius: 21px; + pointer-events: none; + } +`; + +const CardImageLayer = styled(motion.div)` + position: absolute; + inset: 0; +`; + +const skeletonShimmer = keyframes` 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } `; @@ -184,7 +184,7 @@ const SkeletonImage = styled(SkeletonSurface)` inset: 0; `; -const SkeletonBottomPanel = styled.div<{ $isActive: boolean }>` +const SkeletonBottomPanel = styled.div<{ $isActive: boolean }>` position: absolute; right: 0; bottom: 0; @@ -192,10 +192,10 @@ const SkeletonBottomPanel = styled.div<{ $isActive: boolean }>` box-sizing: border-box; width: 100%; padding: 0.875rem 1rem; - background: ${({ $isActive }) => ($isActive ? '#f5f5f4' : '#f8f8f7')}; + background: ${({ $isActive }) => ($isActive ? '#f5f5f4' : '#f8f8f7')}; - [data-theme='dark'] & { - background: ${surface.dark.surface}; + [data-theme='dark'] & { + background: ${surface.dark.surface}; } @media (min-width: 640px) { @@ -259,26 +259,26 @@ const EditorialRailSkeletonCard = React.memo( }} transition={{ duration: 0 }} $isActive={isActive} - > - - - - - - - - - - {isActive && } - - - + > + + + + + + + + + + {isActive && } + + + ); }, ); -const CardBottomPanel = styled.div<{ $isActive: boolean }>` +const CardBottomPanel = styled.div<{ $isActive: boolean }>` position: absolute; bottom: 0; left: 0; @@ -286,19 +286,19 @@ const CardBottomPanel = styled.div<{ $isActive: boolean }>` box-sizing: border-box; width: 100%; padding: 0.875rem 1rem; - color: ${meok[900]}; - backdrop-filter: blur(24px); - - background-color: ${({ $isActive }) => - $isActive ? 'rgba(255, 248, 245, 0.88)' : 'rgba(255, 255, 255, 0.72)'}; - - [data-theme='dark'] & { - color: ${meok[100]}; - background-color: ${surface.dark.card}; - backdrop-filter: none; - border: 0 solid ${surface.dark.card}; - border-top: 1px solid rgba(255, 255, 255, 0.08); - } + color: ${meok[900]}; + backdrop-filter: blur(24px); + + background-color: ${({ $isActive }) => + $isActive ? 'rgba(255, 248, 245, 0.88)' : 'rgba(255, 255, 255, 0.72)'}; + + [data-theme='dark'] & { + color: ${meok[100]}; + background-color: ${surface.dark.card}; + backdrop-filter: none; + border: 0 solid ${surface.dark.card}; + border-top: 1px solid rgba(255, 255, 255, 0.08); + } @media (min-width: 640px) { padding: 1rem 1.15rem 1.15rem; @@ -326,107 +326,107 @@ const CarouselStageWrapper = styled.div` } [data-theme='dark'] & { - background-color: transparent; + background-color: transparent; + } +`; + +const NavSideButton = styled.button<{ $side: 'left' | 'right' }>` + position: absolute; + top: 0; + bottom: 0; + z-index: 40; + display: flex; + width: 3.5rem; + cursor: pointer; + align-items: center; + border: none; + transition: opacity 0.2s ease; + + @media (min-width: 640px) { + width: 4.5rem; + } + + @media (min-width: 1024px) { + width: 5.5rem; + } + + ${({ $side }) => + $side === 'left' + ? ` + left: 0; + justify-content: flex-start; + padding-left: 0.5rem; + background: transparent; + + @media (min-width: 640px) { + padding-left: 0.75rem; + } + ` + : ` + right: 0; + justify-content: flex-end; + padding-right: 0.5rem; + background: transparent; + + @media (min-width: 640px) { + padding-right: 0.75rem; + } + `} + + [data-theme='dark'] & { + background: transparent; + } + + &:active:not(:disabled) { + opacity: 0.8; + } + + &:disabled { + cursor: default; + opacity: 0; + pointer-events: none; + } + + span.icon-box { + display: flex; + height: 2.75rem; + width: 2.25rem; + align-items: center; + justify-content: center; + border: 1px solid rgba(255, 255, 255, 0.72); + border-radius: 0.75rem; + background-color: rgba(248, 248, 247, 0.8); + color: ${meok[900]}; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + transition: color 0.2s ease; + + [data-theme='dark'] & { + border-color: rgba(255, 255, 255, 0.08); + background-color: rgba(45, 45, 43, 0.86); + color: ${meok[200]}; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.25); + } + } + + &:hover:not(:disabled) span.icon-box { + color: ${palette.juhong[500]}; + + [data-theme='dark'] & { + color: ${palette.juhong[400]}; + } + } + + @media (prefers-reduced-motion: reduce) { + transition: none; + + span.icon-box { + transition: none; + } } `; -const NavSideButton = styled.button<{ $side: 'left' | 'right' }>` - position: absolute; - top: 0; - bottom: 0; - z-index: 40; - display: flex; - width: 3.5rem; - cursor: pointer; - align-items: center; - border: none; - transition: opacity 0.2s ease; - - @media (min-width: 640px) { - width: 4.5rem; - } - - @media (min-width: 1024px) { - width: 5.5rem; - } - - ${({ $side }) => - $side === 'left' - ? ` - left: 0; - justify-content: flex-start; - padding-left: 0.5rem; - background: transparent; - - @media (min-width: 640px) { - padding-left: 0.75rem; - } - ` - : ` - right: 0; - justify-content: flex-end; - padding-right: 0.5rem; - background: transparent; - - @media (min-width: 640px) { - padding-right: 0.75rem; - } - `} - - [data-theme='dark'] & { - background: transparent; - } - - &:active:not(:disabled) { - opacity: 0.8; - } - - &:disabled { - cursor: default; - opacity: 0; - pointer-events: none; - } - - span.icon-box { - display: flex; - height: 2.75rem; - width: 2.25rem; - align-items: center; - justify-content: center; - border: 1px solid rgba(255, 255, 255, 0.72); - border-radius: 0.75rem; - background-color: rgba(248, 248, 247, 0.8); - color: ${meok[900]}; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08); - backdrop-filter: blur(8px); - -webkit-backdrop-filter: blur(8px); - transition: color 0.2s ease; - - [data-theme='dark'] & { - border-color: rgba(255, 255, 255, 0.08); - background-color: rgba(45, 45, 43, 0.86); - color: ${meok[200]}; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.25); - } - } - - &:hover:not(:disabled) span.icon-box { - color: ${palette.juhong[500]}; - - [data-theme='dark'] & { - color: ${palette.juhong[400]}; - } - } - - @media (prefers-reduced-motion: reduce) { - transition: none; - - span.icon-box { - transition: none; - } - } -`; - const CategoryTabButton = styled.button<{ $isSelected: boolean }>` user-select: none; white-space: nowrap; @@ -498,47 +498,47 @@ const EditorialRailCard = React.memo( draggable={false} onMouseDown={(event) => event.preventDefault()} aria-label={`${story.title}${isActive ? ' 현재 선택됨' : ''}`} - > - - - { - const image = event.currentTarget; - if (image.dataset.fallbackApplied === 'true') { - image.onerror = null; - image.src = FALLBACK_IMAGE_SETS.default[0]; - return; - } - image.dataset.fallbackApplied = 'true'; - image.src = fallbackImageFor(story); - }} - /> -
- - - + > + + + { + const image = event.currentTarget; + if (image.dataset.fallbackApplied === 'true') { + image.onerror = null; + image.src = FALLBACK_IMAGE_SETS.default[0]; + return; + } + image.dataset.fallbackApplied = 'true'; + image.src = fallbackImageFor(story); + }} + /> +
+ + +

( display: 'inline-flex', alignItems: 'center', gap: 8, - fontSize: fontSize.micro, + fontSize: typographyRole.caption.fontSize, color: palette.juhong[500], }} > {copy.durationText} ↗ )} - - - - + + + + ); }, (previous, next) => { @@ -615,7 +615,7 @@ const CardSub = styled.p` overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; line-height: 1rem; color: ${meok[700]}; @@ -637,27 +637,27 @@ const IndicatorDot = styled.button<{ $active: boolean }>` width: 2rem; background-color: ${palette.juhong[500]}; ` - : ` - width: 6px; - background-color: rgba(33, 30, 25, 0.2); - &:hover { - background-color: rgba(33, 30, 25, 0.5); - } - `} - - [data-theme='dark'] & { - background-color: ${({ $active }) => - $active ? palette.juhong[500] : 'rgba(250, 250, 250, 0.32)'}; - - &:hover { - background-color: ${({ $active }) => - $active ? palette.juhong[500] : 'rgba(250, 250, 250, 0.56)'}; - } - } -`; + : ` + width: 6px; + background-color: rgba(33, 30, 25, 0.2); + &:hover { + background-color: rgba(33, 30, 25, 0.5); + } + `} + + [data-theme='dark'] & { + background-color: ${({ $active }) => + $active ? palette.juhong[500] : 'rgba(250, 250, 250, 0.32)'}; + + &:hover { + background-color: ${({ $active }) => + $active ? palette.juhong[500] : 'rgba(250, 250, 250, 0.56)'}; + } + } +`; export const SorimaruEditorialRail = React.memo( - function SorimaruEditorialRail({ stories, storySets, isActive = true, isLoading = false, error = null, onRetry, onSelectStory }) { + function SorimaruEditorialRail({ stories, storySets, isActive = true, isLoading = false, error = null, onRetry, onSelectStory }) { const [selectedKeyword, setSelectedKeyword] = useState(SORIMARU_THEME_CATEGORIES[0].keyword); const [cachedImageUrls, setCachedImageUrls] = useState>(() => { if (typeof window === 'undefined') return {}; @@ -807,10 +807,10 @@ export const SorimaruEditorialRail = React.memo( ); useEffect(() => { - if (!isActive || featured.length < 2 || isLoading) return; - const timer = window.setInterval(() => moveBy(1, false), 7000); - return () => window.clearInterval(timer); - }, [autoResetToken, featured.length, isActive, isLoading, moveBy]); + if (!isActive || featured.length < 2 || isLoading) return; + const timer = window.setInterval(() => moveBy(1, false), 7000); + return () => window.clearInterval(timer); + }, [autoResetToken, featured.length, isActive, isLoading, moveBy]); useEffect( () => () => { @@ -829,8 +829,8 @@ export const SorimaruEditorialRail = React.memo( useLayoutEffect(() => { - const el = trackRef.current; - if (!el || !isActive) return undefined; + const el = trackRef.current; + if (!el || !isActive) return undefined; const targetX = -trackMetrics.cardStep * activePosition; @@ -855,7 +855,7 @@ export const SorimaruEditorialRail = React.memo( return () => { tween.kill(); }; - }, [activePosition, isActive, trackMetrics.cardStep, trackTransitionEnabled]); + }, [activePosition, isActive, trackMetrics.cardStep, trackTransitionEnabled]); const handleCategoryChange = (keyword: string) => { if (keyword === selectedKeyword || inputLockedRef.current) return; @@ -879,28 +879,28 @@ export const SorimaruEditorialRail = React.memo( if (offset === 0 && story) onSelectStory(story, 'play'); }; - if (error && !showSkeleton && onRetry) { - return ( -

- -
- ); - } - + if (error && !showSkeleton && onRetry) { + return ( +
+ +
+ ); + } + if (!activeStory && !showSkeleton) { return (
(
- moveBy(-1)} - onDragStart={(event) => event.preventDefault()} - draggable={false} - disabled={showSkeleton || featured.length < 2} - aria-label="이전 이야기" - $side="left" - > - - - - - - moveBy(1)} - onDragStart={(event) => event.preventDefault()} - draggable={false} - disabled={showSkeleton || featured.length < 2} - aria-label="다음 이야기" - $side="right" - > - - - - - + moveBy(-1)} + onDragStart={(event) => event.preventDefault()} + draggable={false} + disabled={showSkeleton || featured.length < 2} + aria-label="이전 이야기" + $side="left" + > + + + + + + moveBy(1)} + onDragStart={(event) => event.preventDefault()} + draggable={false} + disabled={showSkeleton || featured.length < 2} + aria-label="다음 이야기" + $side="right" + > + + + + +
( ); } -); +); diff --git a/src/private/core-ui/sorimaru/SoundConstellationSection.tsx b/src/private/core-ui/sorimaru/SoundConstellationSection.tsx index 1050c2e5..b3fc02ec 100644 --- a/src/private/core-ui/sorimaru/SoundConstellationSection.tsx +++ b/src/private/core-ui/sorimaru/SoundConstellationSection.tsx @@ -1,305 +1,305 @@ -'use client'; - -import React, { useEffect, useMemo, useRef, useState } from 'react'; -import styled from '@emotion/styled'; -import { keyframes } from '@emotion/react'; -import { getSorimaruFallbackImage } from '@/features/sorimaru-audio/hooks/useSorimaruImage'; -import { motion, type Variants } from 'framer-motion'; -import { useSorimaruAudioStore } from '@/features/sorimaru-audio/store/useSorimaruAudioStore'; -import type { SorimaruStorySummary } from '@/features/sorimaru-audio/domain/sorimaruStory'; +'use client'; + +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import styled from '@emotion/styled'; +import { keyframes } from '@emotion/react'; +import { getSorimaruFallbackImage } from '@/features/sorimaru-audio/hooks/useSorimaruImage'; +import { motion, type Variants } from 'framer-motion'; +import { useSorimaruAudioStore } from '@/features/sorimaru-audio/store/useSorimaruAudioStore'; +import type { SorimaruStorySummary } from '@/features/sorimaru-audio/domain/sorimaruStory'; import type { RegionGroupsState, RegionStoriesState } from '@/features/sorimaru-audio/presentation/hooks/useSorimaruRegionStories'; import { OniSearchEmpty } from '@/shared/components/OniSearchEmpty/OniSearchEmpty'; import { SectionErrorFallback } from '@/shared/components/SectionErrorFallback/SectionErrorFallback'; -import { KOREA_MAP_VIEWBOX, KOREA_REGION_PATHS } from './koreaMapPaths'; -import { getVirtualRange, VIRTUAL_ITEM_HEIGHT } from './soundConstellationScroll'; -import { - getRegionPathMotion, -} from './soundConstellationMotion'; -import { palette, meok, surface, fontSize, ringShadow } from '@/design-system/tokens'; - -export interface SoundConstellationSectionProps { - groupsState: RegionGroupsState; - regionStoriesState: RegionStoriesState; - selectedRegionId: string; - onSelectRegion(regionId: string): void; - onLoadMore(): void; - onSelectStory(story: SorimaruStorySummary): void; - onRetry(): void; -} - -const [VB_WIDTH, VB_HEIGHT] = KOREA_MAP_VIEWBOX.split(' ').slice(2).map(Number); -const LIST_EDGE_INSET = 23; - - - - -const pulseKeyframe = keyframes` - 0%, 100% { opacity: 1; transform: scale(1); } - 50% { opacity: 0.4; transform: scale(1.15); } -`; - -const spinKeyframe = keyframes` - from { transform: rotate(0deg); } - to { transform: rotate(360deg); } -`; - -const shimmerKeyframe = keyframes` - 0% { background-position: -200% 0; } - 100% { background-position: 200% 0; } -`; - -const SectionWrapper = styled(motion.section)` - width: 100%; - padding: 2.5rem 0; - - @media (min-width: 640px) { - padding: 3.5rem 0; - } -`; - -const InnerContainer = styled(motion.div)` - margin: 0 auto; - width: min(calc(100% - 40px), 1140px); - max-width: 1140px; - padding: 0; - - @media (max-width: 1024px) { - width: calc(100% - 28px); - } - - @media (max-width: 640px) { - width: calc(100% - 24px); - } -`; - -const sectionVariants: Variants = { - hidden: {}, - visible: { - transition: { - staggerChildren: 0.18, - delayChildren: 0.05, - }, - }, -}; - -const titleVariants: Variants = { - hidden: { opacity: 0, y: 26 }, - visible: { - opacity: 1, - y: 0, - transition: { - duration: 1.0, - ease: [0.16, 1, 0.3, 1], - }, - }, -}; - -const contentVariants: Variants = { - hidden: { opacity: 0, y: 32 }, - visible: { - opacity: 1, - y: 0, - transition: { - duration: 1.15, - ease: [0.16, 1, 0.3, 1], - }, - }, -}; - -const SectionHeader = styled(motion.div)` - padding-bottom: 0.25rem; -`; - -const MainHeading = styled.h2` - display: inline-block; - background: linear-gradient(to right, #211e19, #403b35, #6a6158); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - font-family: inherit; - font-size: clamp(24px, 3.2vw, 36px); - font-weight: 700; - letter-spacing: -0.045em; - - [data-theme='dark'] & { - background: linear-gradient(to right, #ffffff, #d9d9d7, #b0b8c1); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - } -`; - -const SubDesc = styled.p` - margin-top: 0.25rem; - max-width: 36rem; - font-size: ${fontSize.xs}; - line-height: 1.25rem; - color: ${meok[700]}; - - [data-theme='dark'] & { - color: ${meok[400]}; - } - - @media (min-width: 640px) { - font-size: ${fontSize.sm}; - } -`; - -const LayoutGrid = styled(motion.div)` - margin-top: 1.25rem; - display: grid; - gap: 1.25rem; - - @media (min-width: 1024px) { - grid-template-columns: minmax(0, 1.2fr) minmax(320px, 0.8fr); - gap: 1.5rem; - } -`; - -const MapStage = styled.div` - position: relative; - min-height: 480px; - padding: 1rem; - - @media (min-width: 640px) { - min-height: 560px; - padding: 1.5rem; - } -`; - -const MapHintPill = styled.div` - position: absolute; - left: 1rem; - top: 1rem; - z-index: 20; - display: flex; - align-items: center; - gap: 0.5rem; - border-radius: 9999px; - background-color: rgba(255, 255, 255, 0.9); - padding: 0.5rem 0.875rem; - font-size: ${fontSize.xs}; - color: ${meok[700]}; - backdrop-filter: blur(4px); - - [data-theme='dark'] & { - background-color: rgba(23, 30, 43, 0.9); - color: ${meok[200]}; - border: 1px solid rgba(255, 255, 255, 0.08); - } - - @media (min-width: 640px) { - left: 1.5rem; - top: 1.5rem; - } -`; - -const PulseDot = styled.span` - width: 0.375rem; - height: 0.375rem; - border-radius: 9999px; - background: linear-gradient(135deg, #FF7830 0%, #FFA36B 100%); - animation: ${pulseKeyframe} 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; -`; - -const MapSvgWrapper = styled.div` - position: relative; - margin: 3.5rem auto 0; - aspect-ratio: 800 / 759; - width: 100%; - max-width: 520px; - - @media (min-width: 640px) { - margin-top: 3rem; - } -`; - -const StyledSvg = styled.svg` - position: absolute; - inset: 0; - width: 100%; - height: 100%; - overflow: visible; - - [data-theme='dark'] & path { - stroke: rgba(255, 255, 255, 0.25); - } -`; - -const RegionPin = styled.div` - position: absolute; - z-index: 10; - transform: translate(-50%, -50%); -`; - -const RegionPinButton = styled.button<{ isActive: boolean }>` - border: none; - border-radius: 9999px; - padding: 0.375rem 0.65rem; - font-size: ${fontSize.micro}; - font-weight: ${(props) => (props.isActive ? 700 : 600)}; - backdrop-filter: blur(12px); - outline: none; - cursor: pointer; - transition: all 0.25s cubic-bezier(0.16, 1, 0.3, 1); - background: ${(props) => (props.isActive ? palette.juhong[500] : '#ffffff')}; - color: ${(props) => (props.isActive ? '#ffffff' : meok[900])}; - box-shadow: ${(props) => - props.isActive - ? '0 4px 16px rgba(255, 85, 0, 0.4), 0 2px 6px rgba(0, 0, 0, 0.12)' - : '0 2px 10px rgba(0, 0, 0, 0.12), 0 1px 4px rgba(0, 0, 0, 0.06)'}; - transform: ${(props) => (props.isActive ? 'scale(1.08)' : 'scale(1)')}; - - [data-theme='dark'] & { - background: ${(props) => (props.isActive ? palette.juhong[500] : '#ffffff')}; - color: ${(props) => (props.isActive ? '#ffffff' : '#171513')}; - box-shadow: ${(props) => - props.isActive - ? '0 4px 20px rgba(255, 85, 0, 0.55), 0 0 12px rgba(255, 85, 0, 0.35)' - : '0 3px 14px rgba(0, 0, 0, 0.5), 0 1px 5px rgba(0, 0, 0, 0.3)'}; - } - - &:hover { - transform: scale(1.1); - background: ${(props) => (props.isActive ? palette.juhong[600] : '#ffffff')}; - color: ${(props) => (props.isActive ? '#ffffff' : palette.juhong[500])}; - box-shadow: 0 6px 20px rgba(0, 0, 0, 0.18); - - [data-theme='dark'] & { - background: ${(props) => (props.isActive ? palette.juhong[600] : '#ffffff')}; - color: ${(props) => (props.isActive ? '#ffffff' : palette.juhong[500])}; - box-shadow: 0 6px 22px rgba(0, 0, 0, 0.5); - } - } - - @media (min-width: 640px) { - padding: 0.4rem 0.8rem; - font-size: ${fontSize.xs}; - } -`; - -const AsidePanel = styled.aside` - display: flex; - height: 480px; - flex-direction: column; - border-radius: 1rem; - overflow: hidden; - background-color: rgba(255, 255, 255, 0.85); +import { KOREA_MAP_VIEWBOX, KOREA_REGION_PATHS } from './koreaMapPaths'; +import { getVirtualRange, VIRTUAL_ITEM_HEIGHT } from './soundConstellationScroll'; +import { + getRegionPathMotion, +} from './soundConstellationMotion'; +import { palette, meok, typographyRole, ringShadow } from '@/design-system/tokens'; + +export interface SoundConstellationSectionProps { + groupsState: RegionGroupsState; + regionStoriesState: RegionStoriesState; + selectedRegionId: string; + onSelectRegion(regionId: string): void; + onLoadMore(): void; + onSelectStory(story: SorimaruStorySummary): void; + onRetry(): void; +} + +const [VB_WIDTH, VB_HEIGHT] = KOREA_MAP_VIEWBOX.split(' ').slice(2).map(Number); +const LIST_EDGE_INSET = 23; + + + + +const pulseKeyframe = keyframes` + 0%, 100% { opacity: 1; transform: scale(1); } + 50% { opacity: 0.4; transform: scale(1.15); } +`; + +const spinKeyframe = keyframes` + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +`; + +const shimmerKeyframe = keyframes` + 0% { background-position: -200% 0; } + 100% { background-position: 200% 0; } +`; + +const SectionWrapper = styled(motion.section)` + width: 100%; + padding: 2.5rem 0; + + @media (min-width: 640px) { + padding: 3.5rem 0; + } +`; + +const InnerContainer = styled(motion.div)` + margin: 0 auto; + width: min(calc(100% - 40px), 1140px); + max-width: 1140px; + padding: 0; + + @media (max-width: 1024px) { + width: calc(100% - 28px); + } + + @media (max-width: 640px) { + width: calc(100% - 24px); + } +`; + +const sectionVariants: Variants = { + hidden: {}, + visible: { + transition: { + staggerChildren: 0.18, + delayChildren: 0.05, + }, + }, +}; + +const titleVariants: Variants = { + hidden: { opacity: 0, y: 26 }, + visible: { + opacity: 1, + y: 0, + transition: { + duration: 1.0, + ease: [0.16, 1, 0.3, 1], + }, + }, +}; + +const contentVariants: Variants = { + hidden: { opacity: 0, y: 32 }, + visible: { + opacity: 1, + y: 0, + transition: { + duration: 1.15, + ease: [0.16, 1, 0.3, 1], + }, + }, +}; + +const SectionHeader = styled(motion.div)` + padding-bottom: 0.25rem; +`; + +const MainHeading = styled.h2` + display: inline-block; + background: linear-gradient(to right, #211e19, #403b35, #6a6158); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + font-family: inherit; + font-size: clamp(24px, 3.2vw, 36px); + font-weight: 700; + letter-spacing: -0.045em; + + [data-theme='dark'] & { + background: linear-gradient(to right, #ffffff, #d9d9d7, #b0b8c1); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + } +`; + +const SubDesc = styled.p` + margin-top: 0.25rem; + max-width: 36rem; + font-size: ${typographyRole.label.fontSize}; + line-height: 1.25rem; + color: ${meok[700]}; + + [data-theme='dark'] & { + color: ${meok[400]}; + } + + @media (min-width: 640px) { + font-size: ${typographyRole.bodySm.fontSize}; + } +`; + +const LayoutGrid = styled(motion.div)` + margin-top: 1.25rem; + display: grid; + gap: 1.25rem; + + @media (min-width: 1024px) { + grid-template-columns: minmax(0, 1.2fr) minmax(320px, 0.8fr); + gap: 1.5rem; + } +`; + +const MapStage = styled.div` + position: relative; + min-height: 480px; + padding: 1rem; + + @media (min-width: 640px) { + min-height: 560px; + padding: 1.5rem; + } +`; + +const MapHintPill = styled.div` + position: absolute; + left: 1rem; + top: 1rem; + z-index: 20; + display: flex; + align-items: center; + gap: 0.5rem; + border-radius: 9999px; + background-color: rgba(255, 255, 255, 0.9); + padding: 0.5rem 0.875rem; + font-size: ${typographyRole.label.fontSize}; + color: ${meok[700]}; + backdrop-filter: blur(4px); + + [data-theme='dark'] & { + background-color: rgba(23, 30, 43, 0.9); + color: ${meok[200]}; + border: 1px solid rgba(255, 255, 255, 0.08); + } + + @media (min-width: 640px) { + left: 1.5rem; + top: 1.5rem; + } +`; + +const PulseDot = styled.span` + width: 0.375rem; + height: 0.375rem; + border-radius: 9999px; + background: linear-gradient(135deg, #FF7830 0%, #FFA36B 100%); + animation: ${pulseKeyframe} 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; +`; + +const MapSvgWrapper = styled.div` + position: relative; + margin: 3.5rem auto 0; + aspect-ratio: 800 / 759; + width: 100%; + max-width: 520px; + + @media (min-width: 640px) { + margin-top: 3rem; + } +`; + +const StyledSvg = styled.svg` + position: absolute; + inset: 0; + width: 100%; + height: 100%; + overflow: visible; + + [data-theme='dark'] & path { + stroke: rgba(255, 255, 255, 0.25); + } +`; + +const RegionPin = styled.div` + position: absolute; + z-index: 10; + transform: translate(-50%, -50%); +`; + +const RegionPinButton = styled.button<{ isActive: boolean }>` + border: none; + border-radius: 9999px; + padding: 0.375rem 0.65rem; + font-size: ${typographyRole.caption.fontSize}; + font-weight: ${(props) => (props.isActive ? 700 : 600)}; + backdrop-filter: blur(12px); + outline: none; + cursor: pointer; + transition: all 0.25s cubic-bezier(0.16, 1, 0.3, 1); + background: ${(props) => (props.isActive ? palette.juhong[500] : '#ffffff')}; + color: ${(props) => (props.isActive ? '#ffffff' : meok[900])}; + box-shadow: ${(props) => + props.isActive + ? '0 4px 16px rgba(255, 85, 0, 0.4), 0 2px 6px rgba(0, 0, 0, 0.12)' + : '0 2px 10px rgba(0, 0, 0, 0.12), 0 1px 4px rgba(0, 0, 0, 0.06)'}; + transform: ${(props) => (props.isActive ? 'scale(1.08)' : 'scale(1)')}; + + [data-theme='dark'] & { + background: ${(props) => (props.isActive ? palette.juhong[500] : '#ffffff')}; + color: ${(props) => (props.isActive ? '#ffffff' : '#171513')}; + box-shadow: ${(props) => + props.isActive + ? '0 4px 20px rgba(255, 85, 0, 0.55), 0 0 12px rgba(255, 85, 0, 0.35)' + : '0 3px 14px rgba(0, 0, 0, 0.5), 0 1px 5px rgba(0, 0, 0, 0.3)'}; + } + + &:hover { + transform: scale(1.1); + background: ${(props) => (props.isActive ? palette.juhong[600] : '#ffffff')}; + color: ${(props) => (props.isActive ? '#ffffff' : palette.juhong[500])}; + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.18); + + [data-theme='dark'] & { + background: ${(props) => (props.isActive ? palette.juhong[600] : '#ffffff')}; + color: ${(props) => (props.isActive ? '#ffffff' : palette.juhong[500])}; + box-shadow: 0 6px 22px rgba(0, 0, 0, 0.5); + } + } + + @media (min-width: 640px) { + padding: 0.4rem 0.8rem; + font-size: ${typographyRole.label.fontSize}; + } +`; + +const AsidePanel = styled.aside` + display: flex; + height: 480px; + flex-direction: column; + border-radius: 1rem; + overflow: hidden; + background-color: rgba(255, 255, 255, 0.85); border: 1px solid #e5e5e3; - box-shadow: none; - padding: 1rem 0.25rem; - backdrop-filter: blur(12px); - - [data-theme='dark'] & { - background-color: rgba(23, 30, 43, 0.88); + box-shadow: none; + padding: 1rem 0.25rem; + backdrop-filter: blur(12px); + + [data-theme='dark'] & { + background-color: rgba(23, 30, 43, 0.88); border: 1px solid rgba(255, 255, 255, 0.12); - box-shadow: none; - } - - @media (min-width: 640px) { - height: 560px; - padding: 1.25rem 0.25rem; - } -`; - + box-shadow: none; + } + + @media (min-width: 640px) { + height: 560px; + padding: 1.25rem 0.25rem; + } +`; + const RegionErrorFallback = styled(SectionErrorFallback)` &&, [data-theme='dark'] && { @@ -308,324 +308,324 @@ const RegionErrorFallback = styled(SectionErrorFallback)` } `; -const AsideHeader = styled.div` - margin: 0 0.5rem; - padding-bottom: 0.75rem; - flex-shrink: 0; - display: flex; - align-items: flex-end; - justify-content: space-between; - gap: 0.75rem; - padding-left: 0.25rem; - padding-right: 0.25rem; -`; - -const RegionLabel = styled.h3` - font-size: ${fontSize.xl}; - font-weight: 800; - letter-spacing: -0.04em; - color: ${meok[900]}; - - [data-theme='dark'] & { - color: ${meok[100]}; - } -`; - -const StoriesCount = styled.span` - font-family: var(--font-hanok); - font-variant-numeric: tabular-nums; - font-size: ${fontSize.xs}; - font-weight: 700; - color: ${palette.juhong[500]}; -`; - -const ScrollWrapper = styled.div` - position: relative; - flex: 1; - min-height: 0; - overflow: hidden; - margin-top: 0.25rem; - padding: 0 0.125rem; -`; - -const TopGradientFade = styled.div` - pointer-events: none; - position: absolute; - left: 0; - right: 0; - top: 0; - z-index: 20; - height: 28px; - background: linear-gradient(to bottom, rgba(255, 255, 255, 1) 40%, rgba(255, 255, 255, 0)); - - [data-theme='dark'] & { - background: linear-gradient(to bottom, rgba(23, 30, 43, 1) 40%, rgba(23, 30, 43, 0)); - } -`; - -const BottomGradientFade = styled.div` - pointer-events: none; - position: absolute; - left: 0; - right: 0; - bottom: 0; - z-index: 20; - height: 28px; - background: linear-gradient(to top, rgba(255, 255, 255, 1) 40%, rgba(255, 255, 255, 0)); - - [data-theme='dark'] & { - background: linear-gradient(to top, rgba(23, 30, 43, 1) 40%, rgba(23, 30, 43, 0)); - } -`; - -const ScrollContent = styled.div` - margin-right: -0.25rem; - height: 100%; - overflow-y: auto; - padding: 28px 10px 28px 2px; - scrollbar-width: none; - - &::-webkit-scrollbar { - display: none; - } -`; - -const StoryItemButton = styled.button<{ isActive: boolean }>` - display: flex; - width: 100%; - align-items: center; - gap: 0.875rem; - border-radius: 0.75rem; - padding: 0.375rem 0.625rem; - text-align: left; - border: none; - cursor: pointer; - transition: all 0.2s ease; - background: ${(props) => - props.isActive ? `linear-gradient(135deg, ${palette.juhong[50]} 0%, #FFF8F5 100%)` : 'transparent'}; - box-shadow: ${(props) => (props.isActive ? 'none' : ringShadow.light.card)}; - - &:hover { - background: ${(props) => - props.isActive ? `linear-gradient(135deg, ${palette.juhong[100]} 0%, ${palette.juhong[50]} 100%)` : meok[200]}; - box-shadow: ${(props) => (props.isActive ? 'none' : ringShadow.light.cardHoverGlow)}; - } - - [data-theme='dark'] & { - border: none; - background: ${(props) => - props.isActive ? 'linear-gradient(135deg, rgba(255, 120, 48, 0.18) 0%, rgba(255, 85, 0, 0.08) 100%)' : 'transparent'}; - box-shadow: ${(props) => (props.isActive ? 'none' : ringShadow.dark.card)}; - - &:hover { - background: ${(props) => - props.isActive - ? 'linear-gradient(135deg, rgba(255, 120, 48, 0.25) 0%, rgba(255, 85, 0, 0.14) 100%)' - : 'rgba(255, 255, 255, 0.06)'}; - box-shadow: ${(props) => (props.isActive ? 'none' : ringShadow.dark.cardHoverGlow)}; - } - } -`; - -const StoryThumb = styled.span` - position: relative; - height: 72px; - width: 72px; - flex-shrink: 0; - overflow: hidden; - border-radius: 8.7px; - background-color: #f0f0ef; - - [data-theme='dark'] & { - background-color: rgba(255, 255, 255, 0.08); - } - - & img { - height: 100%; - width: 100%; - transform: scale(2.6); - object-fit: cover; - } -`; - -const StoryInfo = styled.span` - min-width: 0; - flex: 1; - padding-right: 0.25rem; -`; - -const StoryHeadRow = styled.span` - display: flex; - min-width: 0; - align-items: flex-start; - gap: 0.5rem; -`; - -const StoryTitle = styled.strong<{ isActive: boolean }>` - min-width: 0; - flex: 1; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; - font-family: inherit; - font-size: ${fontSize.sm}; - font-weight: 700; - line-height: 1.35; - color: ${(props) => (props.isActive ? palette.juhong[500] : meok[900])}; - - [data-theme='dark'] & { - color: ${(props) => (props.isActive ? palette.juhong[400] : meok[100])}; - } -`; - -const DurationStatus = styled.span<{ isActive: boolean }>` - margin-top: 1px; - flex-shrink: 0; - font-size: ${fontSize.micro}; - font-weight: 400; - color: ${(props) => (props.isActive ? palette.juhong[500] : meok[500])}; - - [data-theme='dark'] & { - color: ${(props) => (props.isActive ? palette.juhong[400] : meok[400])}; - } -`; - -const ExcerptText = styled.span` - margin-top: 0.25rem; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; - font-size: ${fontSize.micro}; - line-height: 1.4; - color: ${meok[700]}; - - [data-theme='dark'] & { - color: ${meok[400]}; - } -`; - -const LoadingSpinnerWrapper = styled.div` - display: flex; - align-items: center; - justify-content: center; - gap: 0.5rem; - padding: 0.75rem 0; - font-size: ${fontSize.xs}; - font-weight: 700; - color: ${palette.juhong[500]}; - - & .spinner { - width: 0.75rem; - height: 0.75rem; - border-radius: 9999px; - border: 2px solid ${palette.juhong[500]}; - border-top-color: transparent; - animation: ${spinKeyframe} 1s linear infinite; - } -`; - -const SkeletonShimmer = styled.div` - background: linear-gradient(90deg, #e4e4e2 25%, #ecece9 50%, #e4e4e2 75%); - background-size: 200% 100%; - animation: ${shimmerKeyframe} 1.5s infinite; -`; - -const RegionStoryListSkeleton: React.FC = () => ( -
- {Array.from({ length: 5 }, (_, index) => ( -
- -
- - - -
- -
- ))} -
-); - -function getStoryExcerpt(story: SorimaruStorySummary): string { - if (story.audioTitle && story.audioTitle !== story.title) { - return story.audioTitle; - } - return story.region.name ? `${story.region.name}에 남은 오디오 소리 이야기` : '이 장소에 머무는 오디오 이야기'; -} - -function RegionStoryItem({ - story, - active, - isPlaying, - onClick, - onMouseEnter, - onMouseLeave -}: { - story: SorimaruStorySummary; - active: boolean; - isPlaying: boolean; - onClick: () => void; - onMouseEnter: () => void; - onMouseLeave: () => void; -}) { - const fallbackImage = getSorimaruFallbackImage(story); - const imgSrc = story.imageUrl || fallbackImage; - const seconds = Math.max(0, Math.floor(story.durationSeconds)); - const formattedDuration = `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, '0')}`; - return ( - - - { - const image = e.currentTarget; - if (image.dataset.fallbackApplied) return; - image.dataset.fallbackApplied = 'true'; - image.src = fallbackImage; - }} - /> - - - - - {story.title} - - - {active && isPlaying ? '재생 중' : formattedDuration} - - - - {getStoryExcerpt(story)} - - - - ); -} - -export const SoundConstellationSection: React.FC = ({ - groupsState, regionStoriesState, selectedRegionId, onSelectRegion, onLoadMore, onSelectStory, onRetry, -}) => { - const scrollContainerRef = useRef(null); - const indicatorThumbRef = useRef(null); - const scrollFrameRef = useRef(null); - const scrollMetricsRef = useRef<{ scrollTop: number; scrollHeight: number; clientHeight: number } | null>(null); - const paginationPendingRef = useRef(false); - const touchStartYRef = useRef(null); - - const [hoveredRegionId, setHoveredRegionId] = useState(null); - const [isListHovered, setIsListHovered] = useState(false); +const AsideHeader = styled.div` + margin: 0 0.5rem; + padding-bottom: 0.75rem; + flex-shrink: 0; + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 0.75rem; + padding-left: 0.25rem; + padding-right: 0.25rem; +`; + +const RegionLabel = styled.h3` + font-size: ${typographyRole.sectionTitle.fontSize}; + font-weight: 800; + letter-spacing: -0.04em; + color: ${meok[900]}; + + [data-theme='dark'] & { + color: ${meok[100]}; + } +`; + +const StoriesCount = styled.span` + font-family: var(--font-hanok); + font-variant-numeric: tabular-nums; + font-size: ${typographyRole.label.fontSize}; + font-weight: 700; + color: ${palette.juhong[500]}; +`; + +const ScrollWrapper = styled.div` + position: relative; + flex: 1; + min-height: 0; + overflow: hidden; + margin-top: 0.25rem; + padding: 0 0.125rem; +`; + +const TopGradientFade = styled.div` + pointer-events: none; + position: absolute; + left: 0; + right: 0; + top: 0; + z-index: 20; + height: 28px; + background: linear-gradient(to bottom, rgba(255, 255, 255, 1) 40%, rgba(255, 255, 255, 0)); + + [data-theme='dark'] & { + background: linear-gradient(to bottom, rgba(23, 30, 43, 1) 40%, rgba(23, 30, 43, 0)); + } +`; + +const BottomGradientFade = styled.div` + pointer-events: none; + position: absolute; + left: 0; + right: 0; + bottom: 0; + z-index: 20; + height: 28px; + background: linear-gradient(to top, rgba(255, 255, 255, 1) 40%, rgba(255, 255, 255, 0)); + + [data-theme='dark'] & { + background: linear-gradient(to top, rgba(23, 30, 43, 1) 40%, rgba(23, 30, 43, 0)); + } +`; + +const ScrollContent = styled.div` + margin-right: -0.25rem; + height: 100%; + overflow-y: auto; + padding: 28px 10px 28px 2px; + scrollbar-width: none; + + &::-webkit-scrollbar { + display: none; + } +`; + +const StoryItemButton = styled.button<{ isActive: boolean }>` + display: flex; + width: 100%; + align-items: center; + gap: 0.875rem; + border-radius: 0.75rem; + padding: 0.375rem 0.625rem; + text-align: left; + border: none; + cursor: pointer; + transition: all 0.2s ease; + background: ${(props) => + props.isActive ? `linear-gradient(135deg, ${palette.juhong[50]} 0%, #FFF8F5 100%)` : 'transparent'}; + box-shadow: ${(props) => (props.isActive ? 'none' : ringShadow.light.card)}; + + &:hover { + background: ${(props) => + props.isActive ? `linear-gradient(135deg, ${palette.juhong[100]} 0%, ${palette.juhong[50]} 100%)` : meok[200]}; + box-shadow: ${(props) => (props.isActive ? 'none' : ringShadow.light.cardHoverGlow)}; + } + + [data-theme='dark'] & { + border: none; + background: ${(props) => + props.isActive ? 'linear-gradient(135deg, rgba(255, 120, 48, 0.18) 0%, rgba(255, 85, 0, 0.08) 100%)' : 'transparent'}; + box-shadow: ${(props) => (props.isActive ? 'none' : ringShadow.dark.card)}; + + &:hover { + background: ${(props) => + props.isActive + ? 'linear-gradient(135deg, rgba(255, 120, 48, 0.25) 0%, rgba(255, 85, 0, 0.14) 100%)' + : 'rgba(255, 255, 255, 0.06)'}; + box-shadow: ${(props) => (props.isActive ? 'none' : ringShadow.dark.cardHoverGlow)}; + } + } +`; + +const StoryThumb = styled.span` + position: relative; + height: 72px; + width: 72px; + flex-shrink: 0; + overflow: hidden; + border-radius: 8.7px; + background-color: #f0f0ef; + + [data-theme='dark'] & { + background-color: rgba(255, 255, 255, 0.08); + } + + & img { + height: 100%; + width: 100%; + transform: scale(2.6); + object-fit: cover; + } +`; + +const StoryInfo = styled.span` + min-width: 0; + flex: 1; + padding-right: 0.25rem; +`; + +const StoryHeadRow = styled.span` + display: flex; + min-width: 0; + align-items: flex-start; + gap: 0.5rem; +`; + +const StoryTitle = styled.strong<{ isActive: boolean }>` + min-width: 0; + flex: 1; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + font-family: inherit; + font-size: ${typographyRole.bodySm.fontSize}; + font-weight: 700; + line-height: 1.35; + color: ${(props) => (props.isActive ? palette.juhong[500] : meok[900])}; + + [data-theme='dark'] & { + color: ${(props) => (props.isActive ? palette.juhong[400] : meok[100])}; + } +`; + +const DurationStatus = styled.span<{ isActive: boolean }>` + margin-top: 1px; + flex-shrink: 0; + font-size: ${typographyRole.caption.fontSize}; + font-weight: 400; + color: ${(props) => (props.isActive ? palette.juhong[500] : meok[500])}; + + [data-theme='dark'] & { + color: ${(props) => (props.isActive ? palette.juhong[400] : meok[400])}; + } +`; + +const ExcerptText = styled.span` + margin-top: 0.25rem; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + font-size: ${typographyRole.caption.fontSize}; + line-height: 1.4; + color: ${meok[700]}; + + [data-theme='dark'] & { + color: ${meok[400]}; + } +`; + +const LoadingSpinnerWrapper = styled.div` + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 0.75rem 0; + font-size: ${typographyRole.label.fontSize}; + font-weight: 700; + color: ${palette.juhong[500]}; + + & .spinner { + width: 0.75rem; + height: 0.75rem; + border-radius: 9999px; + border: 2px solid ${palette.juhong[500]}; + border-top-color: transparent; + animation: ${spinKeyframe} 1s linear infinite; + } +`; + +const SkeletonShimmer = styled.div` + background: linear-gradient(90deg, #e4e4e2 25%, #ecece9 50%, #e4e4e2 75%); + background-size: 200% 100%; + animation: ${shimmerKeyframe} 1.5s infinite; +`; + +const RegionStoryListSkeleton: React.FC = () => ( +
+ {Array.from({ length: 5 }, (_, index) => ( +
+ +
+ + + +
+ +
+ ))} +
+); + +function getStoryExcerpt(story: SorimaruStorySummary): string { + if (story.audioTitle && story.audioTitle !== story.title) { + return story.audioTitle; + } + return story.region.name ? `${story.region.name}에 남은 오디오 소리 이야기` : '이 장소에 머무는 오디오 이야기'; +} + +function RegionStoryItem({ + story, + active, + isPlaying, + onClick, + onMouseEnter, + onMouseLeave +}: { + story: SorimaruStorySummary; + active: boolean; + isPlaying: boolean; + onClick: () => void; + onMouseEnter: () => void; + onMouseLeave: () => void; +}) { + const fallbackImage = getSorimaruFallbackImage(story); + const imgSrc = story.imageUrl || fallbackImage; + const seconds = Math.max(0, Math.floor(story.durationSeconds)); + const formattedDuration = `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, '0')}`; + return ( + + + { + const image = e.currentTarget; + if (image.dataset.fallbackApplied) return; + image.dataset.fallbackApplied = 'true'; + image.src = fallbackImage; + }} + /> + + + + + {story.title} + + + {active && isPlaying ? '재생 중' : formattedDuration} + + + + {getStoryExcerpt(story)} + + + + ); +} + +export const SoundConstellationSection: React.FC = ({ + groupsState, regionStoriesState, selectedRegionId, onSelectRegion, onLoadMore, onSelectStory, onRetry, +}) => { + const scrollContainerRef = useRef(null); + const indicatorThumbRef = useRef(null); + const scrollFrameRef = useRef(null); + const scrollMetricsRef = useRef<{ scrollTop: number; scrollHeight: number; clientHeight: number } | null>(null); + const paginationPendingRef = useRef(false); + const touchStartYRef = useRef(null); + + const [hoveredRegionId, setHoveredRegionId] = useState(null); + const [isListHovered, setIsListHovered] = useState(false); const regionError = groupsState.error ?? regionStoriesState.error; const isRegionLoading = groupsState.status === 'idle' || groupsState.status === 'loading' @@ -635,263 +635,263 @@ export const SoundConstellationSection: React.FC && groupsState.status === 'success' && regionStoriesState.status === 'empty' && !regionStoriesState.hasMore; - const isFetchingNextPage = regionStoriesState.loadingNext; - - - const [renderScrollTop, setRenderScrollTop] = useState(0); - const [containerHeight, setContainerHeight] = useState(500); - - const regionGroups = groupsState.data; - const hasMore = regionStoriesState.hasMore; - - const currentStory = useSorimaruAudioStore((state) => state.currentStory); - const isPlaying = useSorimaruAudioStore((state) => state.isPlaying); - - const selectedRegion = KOREA_REGION_PATHS.find((region) => region.id === selectedRegionId) || KOREA_REGION_PATHS[0]; - - const selectRegion = (regionId: string) => { - if (regionId === selectedRegionId && (groupsState.error || regionStoriesState.error)) { - onRetry(); - return; - } - if (scrollContainerRef.current) scrollContainerRef.current.scrollTop = 0; - setRenderScrollTop(0); - onSelectRegion(regionId); - }; - - useEffect(() => () => { - if (scrollFrameRef.current !== null) cancelAnimationFrame(scrollFrameRef.current); - }, []); - - useEffect(() => { - if (!isFetchingNextPage) paginationPendingRef.current = false; - }, [isFetchingNextPage, regionStoriesState.items, regionStoriesState.error, selectedRegionId]); - - const requestNextPage = () => { - if (!hasMore || isFetchingNextPage || isRegionLoading || regionStoriesState.error || paginationPendingRef.current) return; - paginationPendingRef.current = true; - onLoadMore(); - }; - - const requestForShortList = (container: HTMLDivElement) => { - if (container.clientHeight > 0 && container.scrollHeight <= container.clientHeight) requestNextPage(); - }; - - const handleWheel = (event: React.WheelEvent) => { - if (event.deltaY > 0) requestForShortList(event.currentTarget); - }; - - const handleTouchEnd = (event: React.TouchEvent) => { - const startY = touchStartYRef.current; - touchStartYRef.current = null; - const endY = event.changedTouches[0]?.clientY; - if (startY !== null && endY !== undefined && startY - endY > 24) requestForShortList(event.currentTarget); - }; - - const handleScroll = (event: React.UIEvent) => { - const target = event.currentTarget; - const currentScrollTop = target.scrollTop; - const currentScrollHeight = target.scrollHeight; - const currentClientHeight = target.clientHeight; - - scrollMetricsRef.current = { - scrollTop: currentScrollTop, - scrollHeight: currentScrollHeight, - clientHeight: currentClientHeight, - }; - - if (scrollFrameRef.current === null) { - scrollFrameRef.current = requestAnimationFrame(() => { - const metrics = scrollMetricsRef.current; - scrollFrameRef.current = null; - if (!metrics) return; - - const trackHeight = Math.max(0, metrics.clientHeight - LIST_EDGE_INSET * 2); - const thumbHeight = Math.max(18, trackHeight * (metrics.clientHeight / metrics.scrollHeight)); - const maxThumbOffset = Math.max(0, trackHeight - thumbHeight); - const maxNativeScrollTop = Math.max(1, metrics.scrollHeight - metrics.clientHeight); - const thumbOffset = (metrics.scrollTop / maxNativeScrollTop) * maxThumbOffset; - - if (indicatorThumbRef.current) { - indicatorThumbRef.current.style.height = `${thumbHeight}px`; - indicatorThumbRef.current.style.transform = `translateY(${thumbOffset}px)`; - } - - setContainerHeight((previous) => previous === metrics.clientHeight ? previous : metrics.clientHeight); - setRenderScrollTop((previous) => { - const previousRange = getVirtualRange(previous, metrics.clientHeight, totalCount); - const nextRange = getVirtualRange(metrics.scrollTop, metrics.clientHeight, totalCount); - return previousRange.startIndex === nextRange.startIndex && previousRange.endIndex === nextRange.endIndex - ? previous - : metrics.scrollTop; - }); - }); - } - - if ( - currentScrollHeight > currentClientHeight && - currentScrollHeight - (currentScrollTop + currentClientHeight) < 120 - ) { - requestNextPage(); - } - }; - - const regionStories = regionStoriesState.items; - const apiRegionGroup = regionGroups?.groups.find((group) => group.label === selectedRegion.label); - - const totalCount = regionStories.length; - const totalHeight = totalCount * VIRTUAL_ITEM_HEIGHT; - const scrollContentHeight = totalHeight + 34; - const indicatorTrackHeight = Math.max(0, containerHeight - LIST_EDGE_INSET * 2); - const canScrollStories = scrollContentHeight > containerHeight; - const indicatorThumbHeight = canScrollStories - ? Math.max(18, indicatorTrackHeight * (containerHeight / scrollContentHeight)) - : 0; - const maxScrollTop = Math.max(1, scrollContentHeight - containerHeight); - const indicatorThumbOffset = canScrollStories - ? (renderScrollTop / maxScrollTop) * Math.max(0, indicatorTrackHeight - indicatorThumbHeight) - : 0; - - const { startIndex, endIndex } = getVirtualRange(renderScrollTop, containerHeight, totalCount); - const visibleStories = useMemo( - () => regionStories.slice(startIndex, endIndex), - [regionStories, startIndex, endIndex] - ); - - return ( - - - - - 지도로 듣는 이야기 - - - 대한민국 지도에서 지역을 눌러 그곳에 남은 오디오 이야기를 들어보세요. - - - - - - - 지역을 눌러 탐색해보세요 - - - - - - {KOREA_REGION_PATHS.map((region) => { - const active = region.id === selectedRegionId; - const apiGroup = regionGroups?.groups.find((group) => group.label === region.label); + const isFetchingNextPage = regionStoriesState.loadingNext; + + + const [renderScrollTop, setRenderScrollTop] = useState(0); + const [containerHeight, setContainerHeight] = useState(500); + + const regionGroups = groupsState.data; + const hasMore = regionStoriesState.hasMore; + + const currentStory = useSorimaruAudioStore((state) => state.currentStory); + const isPlaying = useSorimaruAudioStore((state) => state.isPlaying); + + const selectedRegion = KOREA_REGION_PATHS.find((region) => region.id === selectedRegionId) || KOREA_REGION_PATHS[0]; + + const selectRegion = (regionId: string) => { + if (regionId === selectedRegionId && (groupsState.error || regionStoriesState.error)) { + onRetry(); + return; + } + if (scrollContainerRef.current) scrollContainerRef.current.scrollTop = 0; + setRenderScrollTop(0); + onSelectRegion(regionId); + }; + + useEffect(() => () => { + if (scrollFrameRef.current !== null) cancelAnimationFrame(scrollFrameRef.current); + }, []); + + useEffect(() => { + if (!isFetchingNextPage) paginationPendingRef.current = false; + }, [isFetchingNextPage, regionStoriesState.items, regionStoriesState.error, selectedRegionId]); + + const requestNextPage = () => { + if (!hasMore || isFetchingNextPage || isRegionLoading || regionStoriesState.error || paginationPendingRef.current) return; + paginationPendingRef.current = true; + onLoadMore(); + }; + + const requestForShortList = (container: HTMLDivElement) => { + if (container.clientHeight > 0 && container.scrollHeight <= container.clientHeight) requestNextPage(); + }; + + const handleWheel = (event: React.WheelEvent) => { + if (event.deltaY > 0) requestForShortList(event.currentTarget); + }; + + const handleTouchEnd = (event: React.TouchEvent) => { + const startY = touchStartYRef.current; + touchStartYRef.current = null; + const endY = event.changedTouches[0]?.clientY; + if (startY !== null && endY !== undefined && startY - endY > 24) requestForShortList(event.currentTarget); + }; + + const handleScroll = (event: React.UIEvent) => { + const target = event.currentTarget; + const currentScrollTop = target.scrollTop; + const currentScrollHeight = target.scrollHeight; + const currentClientHeight = target.clientHeight; + + scrollMetricsRef.current = { + scrollTop: currentScrollTop, + scrollHeight: currentScrollHeight, + clientHeight: currentClientHeight, + }; + + if (scrollFrameRef.current === null) { + scrollFrameRef.current = requestAnimationFrame(() => { + const metrics = scrollMetricsRef.current; + scrollFrameRef.current = null; + if (!metrics) return; + + const trackHeight = Math.max(0, metrics.clientHeight - LIST_EDGE_INSET * 2); + const thumbHeight = Math.max(18, trackHeight * (metrics.clientHeight / metrics.scrollHeight)); + const maxThumbOffset = Math.max(0, trackHeight - thumbHeight); + const maxNativeScrollTop = Math.max(1, metrics.scrollHeight - metrics.clientHeight); + const thumbOffset = (metrics.scrollTop / maxNativeScrollTop) * maxThumbOffset; + + if (indicatorThumbRef.current) { + indicatorThumbRef.current.style.height = `${thumbHeight}px`; + indicatorThumbRef.current.style.transform = `translateY(${thumbOffset}px)`; + } + + setContainerHeight((previous) => previous === metrics.clientHeight ? previous : metrics.clientHeight); + setRenderScrollTop((previous) => { + const previousRange = getVirtualRange(previous, metrics.clientHeight, totalCount); + const nextRange = getVirtualRange(metrics.scrollTop, metrics.clientHeight, totalCount); + return previousRange.startIndex === nextRange.startIndex && previousRange.endIndex === nextRange.endIndex + ? previous + : metrics.scrollTop; + }); + }); + } + + if ( + currentScrollHeight > currentClientHeight && + currentScrollHeight - (currentScrollTop + currentClientHeight) < 120 + ) { + requestNextPage(); + } + }; + + const regionStories = regionStoriesState.items; + const apiRegionGroup = regionGroups?.groups.find((group) => group.label === selectedRegion.label); + + const totalCount = regionStories.length; + const totalHeight = totalCount * VIRTUAL_ITEM_HEIGHT; + const scrollContentHeight = totalHeight + 34; + const indicatorTrackHeight = Math.max(0, containerHeight - LIST_EDGE_INSET * 2); + const canScrollStories = scrollContentHeight > containerHeight; + const indicatorThumbHeight = canScrollStories + ? Math.max(18, indicatorTrackHeight * (containerHeight / scrollContentHeight)) + : 0; + const maxScrollTop = Math.max(1, scrollContentHeight - containerHeight); + const indicatorThumbOffset = canScrollStories + ? (renderScrollTop / maxScrollTop) * Math.max(0, indicatorTrackHeight - indicatorThumbHeight) + : 0; + + const { startIndex, endIndex } = getVirtualRange(renderScrollTop, containerHeight, totalCount); + const visibleStories = useMemo( + () => regionStories.slice(startIndex, endIndex), + [regionStories, startIndex, endIndex] + ); + + return ( + + + + + 지도로 듣는 이야기 + + + 대한민국 지도에서 지역을 눌러 그곳에 남은 오디오 이야기를 들어보세요. + + + + + + + 지역을 눌러 탐색해보세요 + + + + + + {KOREA_REGION_PATHS.map((region) => { + const active = region.id === selectedRegionId; + const apiGroup = regionGroups?.groups.find((group) => group.label === region.label); const count = apiGroup?.storyCount ?? ( groupsState.status === 'idle' || groupsState.status === 'loading' ? '…' : groupsState.status === 'error' ? '–' : 0 ); - return ( - - selectRegion(region.id)} - aria-pressed={active} - > - - {region.shortLabel} - {count} - - - - ); - })} - - - - - - {selectedRegion.label} + return ( + + selectRegion(region.id)} + aria-pressed={active} + > + + {region.shortLabel} + {count} + + + + ); + })} + + + + + + {selectedRegion.label} {isRegionLoading ? '조회 중…' @@ -917,95 +917,95 @@ export const SoundConstellationSection: React.FC description="다른 지역을 눌러 새로운 소리를 찾아보세요." /> ) : ( - - - )} - - - - - ); -}; + + + )} + + + + + ); +}; diff --git a/src/shared/components/ContentTagChips/ContentTagChips.tsx b/src/shared/components/ContentTagChips/ContentTagChips.tsx index bfafd9f4..61ec140d 100644 --- a/src/shared/components/ContentTagChips/ContentTagChips.tsx +++ b/src/shared/components/ContentTagChips/ContentTagChips.tsx @@ -1,7 +1,7 @@ 'use client'; import styled from '@emotion/styled'; -import { meok, fontSize } from '@/design-system/tokens'; +import { meok, typographyRole } from '@/design-system/tokens'; @@ -22,7 +22,7 @@ const Chip = styled.span` border-radius: 9999px; background: rgba(78, 89, 104, 0.07); color: ${meok[600]}; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 500; white-space: nowrap; overflow: hidden; diff --git a/src/shared/components/Footer/Footer.tsx b/src/shared/components/Footer/Footer.tsx index 46500254..299a2b63 100644 --- a/src/shared/components/Footer/Footer.tsx +++ b/src/shared/components/Footer/Footer.tsx @@ -4,7 +4,7 @@ import React, { useState, useCallback, useRef, useEffect } from 'react'; import Link from 'next/link'; import { usePathname } from 'next/navigation'; import styled from '@emotion/styled'; -import { fontFamily, fontSize, meok, surface } from '@/design-system/tokens'; +import { fontFamily, typographyRole, meok, surface } from '@/design-system/tokens'; import { useIsAppleDevice } from '@/shared/hooks/useIsAppleDevice'; import PolicyModal, { type PolicyTabKey } from './PolicyModal'; @@ -101,7 +101,7 @@ const NavCol = styled.div` `; const NavColTitle = styled.div` - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 700; letter-spacing: 0.04em; margin-bottom: 2px; @@ -113,7 +113,7 @@ const NavColTitle = styled.div` `; const FooterLink = styled(Link)` - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; text-decoration: none; transition: color 0.18s ease; color: ${meok[600]}; @@ -132,7 +132,7 @@ const FooterLink = styled(Link)` `; const ExternalFooterLink = styled.a` - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; text-decoration: none; transition: color 0.18s ease; color: ${meok[600]}; @@ -184,7 +184,7 @@ const PolicyButton = styled.button<{ $bold?: boolean }>` border: none; padding: 0; font-family: inherit; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: ${({ $bold }) => ($bold ? 700 : 500)}; cursor: pointer; transition: color 0.18s ease; diff --git a/src/shared/components/Footer/PolicyModal.tsx b/src/shared/components/Footer/PolicyModal.tsx index 8a22b812..0687b3f2 100644 --- a/src/shared/components/Footer/PolicyModal.tsx +++ b/src/shared/components/Footer/PolicyModal.tsx @@ -5,7 +5,7 @@ import styled from '@emotion/styled'; import { motion, AnimatePresence } from 'framer-motion'; import { HugeiconsIcon } from '@hugeicons/react' import { Cancel01Icon, CodeIcon, Database01Icon, FileTextIcon, ShieldCheckIcon } from '@hugeicons/core-free-icons' -import { meok, palette, lightPalette, surface, fontSize } from '@/design-system/tokens'; +import { meok, palette, lightPalette, surface, typographyRole } from '@/design-system/tokens'; import { livelyModalSpring, livelyBottomSheetSpring, modalOverlayTransition } from '@/shared/motion/modalMotion'; export type PolicyTabKey = 'privacy' | 'terms' | 'publicData' | 'openSource'; @@ -73,7 +73,7 @@ const ModalHeader = styled.div` `; const HeaderTitle = styled.h2` - font-size: ${fontSize.lg}; + font-size: ${typographyRole.cardTitle.fontSize}; font-weight: 700; color: ${meok[900]}; margin: 0; @@ -142,7 +142,7 @@ const TabButton = styled.button<{ $active: boolean }>` border-radius: 9999px; border: none; font-family: inherit; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: ${({ $active }) => ($active ? 700 : 500)}; cursor: pointer; white-space: nowrap; @@ -170,7 +170,7 @@ const ContentBody = styled.div` overflow-y: auto; padding: 24px clamp(20px, 3.5vw, 32px); color: ${meok[800]}; - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; line-height: 1.7; [data-theme='dark'] & { @@ -191,7 +191,7 @@ const Section = styled.div` margin-bottom: 28px; h3 { - font-size: ${fontSize.base}; + font-size: ${typographyRole.body.fontSize}; font-weight: 700; color: ${meok[900]}; margin: 0 0 10px; @@ -203,7 +203,7 @@ const Section = styled.div` } h4 { - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; font-weight: 600; color: ${meok[800]}; margin: 14px 0 6px; @@ -273,7 +273,7 @@ const CodeBlock = styled.div` `; const MetaDate = styled.div` - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; color: ${meok[500]}; margin-top: 20px; padding-top: 16px; diff --git a/src/shared/components/Header/GlobalMobileTabs.tsx b/src/shared/components/Header/GlobalMobileTabs.tsx index a69968e8..e44c9e03 100644 --- a/src/shared/components/Header/GlobalMobileTabs.tsx +++ b/src/shared/components/Header/GlobalMobileTabs.tsx @@ -7,7 +7,7 @@ import styled from '@emotion/styled'; import { HugeiconsIcon } from '@hugeicons/react' import { BookOpen01Icon, Film01Icon, HeadphonesIcon, Home01Icon, MapIcon, SearchAreaIcon, UserIcon } from '@hugeicons/core-free-icons' import { transientProps } from '@/design-system/styled'; -import { lightPalette, fontSize } from '@/design-system/tokens'; +import { typographyRole } from '@/design-system/tokens'; import { HanokIcon } from '@/features/map/components/HanokIcon'; import { useJourneyStore } from '@/features/journey-curator/store/useJourneyStore'; import { useAuth } from '@/features/auth'; @@ -88,7 +88,7 @@ const TabLink = styled(Link, transientProps)` return $isLanding ? 'rgba(255, 255, 255, 0.14)' : 'rgba(0, 0, 0, 0.07)'; }}; font-family: 'Spoqa Han Sans Neo', sans-serif; - font-size: ${fontSize.micro}; + font-size: ${typographyRole.caption.fontSize}; font-weight: ${({ $isActive }) => ($isActive ? 700 : 400)}; letter-spacing: -0.02em; text-decoration: none; diff --git a/src/shared/components/Header/Header.tsx b/src/shared/components/Header/Header.tsx index 7bfde068..799ece8b 100644 --- a/src/shared/components/Header/Header.tsx +++ b/src/shared/components/Header/Header.tsx @@ -9,7 +9,7 @@ import styled from '@emotion/styled'; import { HugeiconsIcon } from '@hugeicons/react' import { BookOpen01Icon, Cancel01Icon, ChevronRightIcon, Film01Icon, HeadphonesIcon, Home01Icon, MapIcon, Menu01Icon, SearchAreaIcon } from '@hugeicons/core-free-icons' import { transientProps } from '@/design-system/styled'; -import { lightPalette, meok, fontSize, ringShadow } from '@/design-system/tokens'; +import { meok, ringShadow } from '@/design-system/tokens'; import { useOnmaruTheme } from '@/design-system/ThemeProvider'; import { useAuth } from '@/features/auth'; import { OniAvatar } from '@/features/profile/OniAvatar'; diff --git a/src/shared/components/OniSearchEmpty/OniSearchEmpty.tsx b/src/shared/components/OniSearchEmpty/OniSearchEmpty.tsx index bf82c839..20dabab5 100644 --- a/src/shared/components/OniSearchEmpty/OniSearchEmpty.tsx +++ b/src/shared/components/OniSearchEmpty/OniSearchEmpty.tsx @@ -3,7 +3,7 @@ import React, { useRef, useEffect, useState } from 'react'; import styled from '@emotion/styled'; import { useReducedMotion } from 'framer-motion'; -import { meok, fontSize } from '@/design-system/tokens'; +import { meok, typographyRole } from '@/design-system/tokens'; import { useIsAppleDevice } from '@/shared/hooks/useIsAppleDevice'; export interface OniSearchEmptyProps { @@ -112,7 +112,7 @@ const MascotStaticFallback = styled.img` const MainTitle = styled.h4` margin: 0 0 4px; font-family: var(--font-hanok, inherit); - font-size: ${fontSize.lg}; + font-size: ${typographyRole.cardTitle.fontSize}; font-weight: 700; color: ${meok[900]}; letter-spacing: -0.02em; @@ -127,7 +127,7 @@ const MainTitle = styled.h4` const SubDescription = styled.p` margin: 0; - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; font-weight: 400; color: ${meok[600]}; line-height: 1.45; diff --git a/src/shared/components/SectionHeading/SectionHeading.tsx b/src/shared/components/SectionHeading/SectionHeading.tsx index 79cbee7b..73d22271 100644 --- a/src/shared/components/SectionHeading/SectionHeading.tsx +++ b/src/shared/components/SectionHeading/SectionHeading.tsx @@ -2,7 +2,7 @@ import React from 'react'; import styled from '@emotion/styled'; -import { lightPalette, meok, fontSize } from '@/design-system/tokens'; +import { lightPalette, meok, typographyRole } from '@/design-system/tokens'; const Root = styled.div` display: flex; @@ -48,7 +48,7 @@ const Title = styled.h2` const Subtitle = styled.p` margin: 0; color: ${meok[500]}; - font-size: ${fontSize.sm}; + font-size: ${typographyRole.bodySm.fontSize}; line-height: 1.5; [data-theme='dark'] & { @@ -61,7 +61,7 @@ const ActionLink = styled.a` align-items: center; flex-shrink: 0; color: ${lightPalette.juhong[500]}; - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 500; text-decoration: none; white-space: nowrap; diff --git a/src/temp/hanok-archive/sections/HanokDistribution.tsx b/src/temp/hanok-archive/sections/HanokDistribution.tsx index 76953715..f208d644 100644 --- a/src/temp/hanok-archive/sections/HanokDistribution.tsx +++ b/src/temp/hanok-archive/sections/HanokDistribution.tsx @@ -3,7 +3,7 @@ import React, { useMemo } from 'react'; import styled from '@emotion/styled'; import { motion } from 'framer-motion'; -import { meok, palette, fluidHeading, fontSize } from '@/design-system/tokens'; +import { meok, palette, typographyRole } from '@/design-system/tokens'; import SectionHeader from '@/features/hanok-archive/components/SectionHeader'; import { filterLabel } from '@/features/hanok-archive/filterLabels'; import { usePrefersReducedMotion } from '@/hooks/usePrefersReducedMotion'; @@ -48,7 +48,7 @@ const Stat = styled.div` `; const StatLabel = styled.dt` - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 500; letter-spacing: 0.06em; color: ${meok[500]}; @@ -60,7 +60,7 @@ const StatLabel = styled.dt` const StatValue = styled.dd` margin: 0; - font-size: ${fluidHeading.feature}; + font-size: ${typographyRole.display.fontSize}; font-weight: 300; letter-spacing: -0.02em; line-height: 1.1; @@ -85,7 +85,7 @@ const StatUnit = styled.span` const Finding = styled.p` margin: 0 0 clamp(20px, 3vh, 28px); - font-size: ${fontSize.base}; + font-size: ${typographyRole.body.fontSize}; font-weight: 400; line-height: 1.75; color: ${meok[700]}; @@ -174,7 +174,7 @@ const Row = styled(motion.li)` `; const RegionName = styled.span` - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 500; color: ${meok[900]}; white-space: nowrap; @@ -185,7 +185,7 @@ const RegionName = styled.span` `; const Count = styled.span` - font-size: ${fontSize.xs}; + font-size: ${typographyRole.label.fontSize}; font-weight: 400; color: ${meok[700]}; font-variant-numeric: tabular-nums;