From 1e80e448bdc20d8d1ab466280b677b62a5dd92ae Mon Sep 17 00:00:00 2001 From: Shawn Kang Date: Sat, 24 Oct 2026 20:26:08 +0900 Subject: [PATCH 1/2] =?UTF-8?q?design:=20=EA=B3=B5=ED=86=B5=20=ED=83=80?= =?UTF-8?q?=EC=9D=B4=ED=8F=AC=EA=B7=B8=EB=9E=98=ED=94=BC=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=EB=B0=8F=20=EC=A7=84=EC=98=81=20=EC=83=89=EC=83=81?= =?UTF-8?q?=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DesignSystemSample.stories.tsx | 14 ++--- .../ProsAndConsTitle/PropsAndConsTitle.tsx | 8 +-- .../TimerProgressBar.test.tsx | 4 +- .../TimerProgressBar/TimerProgressBar.tsx | 4 +- src/index.css | 56 +++++++++---------- .../AudienceSharePage.test.tsx | 2 +- .../components/AudienceNormalTimer.test.tsx | 2 +- .../AudienceTimeBasedTimerDisplay.test.tsx | 4 +- .../AudienceTimeBasedTimerDisplay.tsx | 2 +- .../components/ReportView.tsx | 16 +++--- .../components/TranscriptView.tsx | 4 +- src/page/DebateTranscriptPage/stanceStyle.ts | 4 +- .../TimerPage/components/CircularTimer.tsx | 4 +- src/page/TimerPage/components/NormalTimer.tsx | 5 +- .../TimerPage/components/TimeBasedTimer.tsx | 4 +- .../TimerPage/components/TimerController.tsx | 4 +- 16 files changed, 66 insertions(+), 71 deletions(-) diff --git a/src/components/DesignSystemSample/DesignSystemSample.stories.tsx b/src/components/DesignSystemSample/DesignSystemSample.stories.tsx index 80cd12f7..59f66801 100644 --- a/src/components/DesignSystemSample/DesignSystemSample.stories.tsx +++ b/src/components/DesignSystemSample/DesignSystemSample.stories.tsx @@ -17,17 +17,17 @@ const ColorItem = ({ className, title }: ColorItemProps) => { const DesignSystemSample = () => (
-

Typography

+

Typography

-

대제목

-

제목

-

부제목

-

세부사항

-

내용

+

Headline · 화면의 대표 제목

+

Title · 구역·내용 묶음의 제목

+

Body · 본문

+

Label · 짧은 조작·항목 이름

+

Caption · 보조 설명

-

Colors

+

Colors

diff --git a/src/components/ProsAndConsTitle/PropsAndConsTitle.tsx b/src/components/ProsAndConsTitle/PropsAndConsTitle.tsx index 10b8d11b..a8406796 100644 --- a/src/components/ProsAndConsTitle/PropsAndConsTitle.tsx +++ b/src/components/ProsAndConsTitle/PropsAndConsTitle.tsx @@ -12,16 +12,16 @@ export default function PropsAndConsTitle({ return (
- + {prosTeamName ?? t('찬성')} -
+
- + {consTeamName ?? t('반대')} -
+
); diff --git a/src/components/TimerProgressBar/TimerProgressBar.test.tsx b/src/components/TimerProgressBar/TimerProgressBar.test.tsx index f1752da3..95102444 100644 --- a/src/components/TimerProgressBar/TimerProgressBar.test.tsx +++ b/src/components/TimerProgressBar/TimerProgressBar.test.tsx @@ -50,8 +50,8 @@ describe('TimerProgressBar', () => { }); it.each([ - ['PROS', 'bg-camp-blue'], - ['CONS', 'bg-camp-red'], + ['PROS', 'bg-stance-blue'], + ['CONS', 'bg-stance-red'], ['DISABLED', 'bg-default-neutral'], ] as const)('%s 팀 색상을 진행 영역에 적용한다', (team, colorClass) => { render(); diff --git a/src/components/TimerProgressBar/TimerProgressBar.tsx b/src/components/TimerProgressBar/TimerProgressBar.tsx index c4ba5112..f95c96f9 100644 --- a/src/components/TimerProgressBar/TimerProgressBar.tsx +++ b/src/components/TimerProgressBar/TimerProgressBar.tsx @@ -18,8 +18,8 @@ interface TimerProgressBarProps { } const TEAM_COLOR_CLASS: Record = { - PROS: 'bg-camp-blue', - CONS: 'bg-camp-red', + PROS: 'bg-stance-blue', + CONS: 'bg-stance-red', DISABLED: 'bg-default-neutral', }; diff --git a/src/index.css b/src/index.css index 2e958ef5..f502e82f 100644 --- a/src/index.css +++ b/src/index.css @@ -5,8 +5,8 @@ --breakpoint-*: initial; --breakpoint-md: 768px; --breakpoint-lg: 1280px; - --color-camp-red: #e14666; - --color-camp-blue: #1e91d6; + --color-stance-red: #e14666; + --color-stance-blue: #1e91d6; --color-brand: #fecd4c; --color-brand-hover: #dba822; --color-default-disabled-hover: #d6d7d9; @@ -22,43 +22,37 @@ --color-semantic-error: #d70000; --color-semantic-table: #ff5622; --color-semantic-material: #6750a4; - --color-blue-500: #3b82f6; - --color-gray-100: #f3f4f6; - --color-gray-200: #e5e7eb; - --color-gray-300: #d1d5db; - --color-gray-400: #9ca3af; - --color-gray-500: #6b7280; - --color-gray-600: #4b5563; - --color-gray-700: #374151; - --color-gray-800: #1f2937; - --color-neutral-100: #f5f5f5; - --color-neutral-200: #e5e5e5; - --color-neutral-300: #d4d4d4; - --color-neutral-400: #a3a3a3; - --color-neutral-50: #fafafa; - --color-neutral-500: #737373; - --color-neutral-600: #525252; - --color-neutral-700: #404040; - --color-neutral-800: #262626; - --color-neutral-900: #171717; - --color-red-500: #ef4444; - --color-slate-50: #f8fafc; - --color-zinc-300: #d4d4d8; - --color-zinc-400: #a1a1aa; --font-pretendard: 'Pretendard Variable', Pretendard, -apple-system, BlinkMacSystemFont, system-ui, Roboto, 'Helvetica Neue', 'Segoe UI', 'Apple SD Gothic Neo', 'Noto Sans KR', 'Malgun Gothic', 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', sans-serif; + /* 공통 글자 역할의 크기·행간·굵기를 함께 정의함. */ + --text-ui-headline: 28px; + --text-ui-headline--line-height: 36px; + --text-ui-headline--font-weight: 700; + --text-ui-title: 24px; + --text-ui-title--line-height: 32px; + --text-ui-title--font-weight: 600; + --text-ui-body: 16px; + --text-ui-body--line-height: 24px; + --text-ui-body--font-weight: 400; + --text-ui-label: 16px; + --text-ui-label--line-height: 24px; + --text-ui-label--font-weight: 600; + --text-ui-caption: 14px; + --text-ui-caption--line-height: 20px; + --text-ui-caption--font-weight: 400; + /* 오래된 타이포그래피이며 최신 타이포그래피로 교체 후 삭제 예정. */ --text-display-raw: 72px; --text-title-raw: 28px; --text-subtitle-raw: 24px; --text-detail-raw: 20px; --text-body-raw: 16px; --text-timer-raw: 110px; - --shadow-camp-blue: 0 0 30px #ff3b2f; - --shadow-camp-red: 0 0 30px #007aff; - --shadow-camp-neutral: 0 0 30px #737373; + --shadow-stance-blue: 0 0 30px #ff3b2f; + --shadow-stance-red: 0 0 30px #007aff; + --shadow-stance-neutral: 0 0 30px #737373; --animate-rotate: rotate 5s linear infinite; --animate-gradient: gradient 10s ease infinite; --animate-color-transition: colorTransition 5s ease-in-out infinite; @@ -148,7 +142,7 @@ } } -/* Text styles */ +/* 오래된 타이포그래피이며 최신 타이포그래피로 교체 후 삭제 예정. */ .text-display { @apply text-display-raw; } @@ -179,11 +173,11 @@ } .timebox.pros { - @apply w-1/2 bg-camp-blue/50; + @apply w-1/2 bg-stance-blue/50; } .timebox.cons { - @apply w-1/2 bg-camp-red/50; + @apply w-1/2 bg-stance-red/50; } .timebox.neutral { diff --git a/src/page/AudienceSharePage/AudienceSharePage.test.tsx b/src/page/AudienceSharePage/AudienceSharePage.test.tsx index 2b531b3e..41176dc3 100644 --- a/src/page/AudienceSharePage/AudienceSharePage.test.tsx +++ b/src/page/AudienceSharePage/AudienceSharePage.test.tsx @@ -388,7 +388,7 @@ describe('AudienceSharePage', () => { expect(screen.getByText('반대 팀')).toBeInTheDocument(); expect(screen.getByText('이토론 토론자')).toBeInTheDocument(); expect(screen.getByTestId('timer-progress-fill')).toHaveClass( - 'bg-camp-red', + 'bg-stance-red', ); }); diff --git a/src/page/AudienceSharePage/components/AudienceNormalTimer.test.tsx b/src/page/AudienceSharePage/components/AudienceNormalTimer.test.tsx index ccb52d2b..5fca15a1 100644 --- a/src/page/AudienceSharePage/components/AudienceNormalTimer.test.tsx +++ b/src/page/AudienceSharePage/components/AudienceNormalTimer.test.tsx @@ -61,7 +61,7 @@ describe('AudienceNormalTimer', () => { screen.getByRole('heading', { name: '사용자 지정 발언' }), ).toBeInTheDocument(); expect(screen.getByTestId('timer-progress-fill')).toHaveClass( - 'bg-camp-red', + 'bg-stance-red', ); }); diff --git a/src/page/AudienceSharePage/components/AudienceTimeBasedTimerDisplay.test.tsx b/src/page/AudienceSharePage/components/AudienceTimeBasedTimerDisplay.test.tsx index 75f32451..ff8dbfda 100644 --- a/src/page/AudienceSharePage/components/AudienceTimeBasedTimerDisplay.test.tsx +++ b/src/page/AudienceSharePage/components/AudienceTimeBasedTimerDisplay.test.tsx @@ -62,7 +62,7 @@ describe('AudienceTimeBasedTimerDisplay', () => { expect(screen.getByText('현재 시간')).toHaveClass( 'md:h-[64px]', 'md:w-[200px]', - 'bg-camp-red', + 'bg-stance-red', 'md:text-[32px]', ); // 모바일(768px 미만, 가로 모드)에서는 배지를 화면에서만 숨기고 스크린 리더에는 남긴다 @@ -154,7 +154,7 @@ describe('AudienceTimeBasedTimerDisplay', () => { expect(screen.getByTestId('pros-speaking-status')).toHaveTextContent( '현재 발언 중', ); - expect(screen.getByText('현재 시간')).toHaveClass('bg-camp-blue'); + expect(screen.getByText('현재 시간')).toHaveClass('bg-stance-blue'); }); it('0초 아래의 값과 100%를 넘는 진행도를 각각 0초와 100%로 제한한다', () => { diff --git a/src/page/AudienceSharePage/components/AudienceTimeBasedTimerDisplay.tsx b/src/page/AudienceSharePage/components/AudienceTimeBasedTimerDisplay.tsx index bd6aee19..d297252b 100644 --- a/src/page/AudienceSharePage/components/AudienceTimeBasedTimerDisplay.tsx +++ b/src/page/AudienceSharePage/components/AudienceTimeBasedTimerDisplay.tsx @@ -74,7 +74,7 @@ export default function AudienceTimeBasedTimerDisplay({ : (currentSpeakingRemainingTime ?? timePerSpeaking); const progress = ((progressBase - progressRemaining) / progressBase) * 100; const activeCurrentBadgeClass = - team === 'PROS' ? 'bg-camp-blue' : 'bg-camp-red'; + team === 'PROS' ? 'bg-stance-blue' : 'bg-stance-red'; return (
= { PROS: { label: '찬성', - border: 'border-camp-blue', + border: 'border-stance-blue', bg: TEAM_STYLE.PROS.baseBg, }, CONS: { label: '반대', - border: 'border-camp-red', + border: 'border-stance-red', bg: TEAM_STYLE.CONS.baseBg, }, }; @@ -208,9 +208,9 @@ export default function ReportView(props: ReportViewProps) { className={clsx( CHIP_CLASS, clash.advantage === 'PROS' && - 'bg-camp-blue text-default-white', + 'bg-stance-blue text-default-white', clash.advantage === 'CONS' && - 'bg-camp-red text-default-white', + 'bg-stance-red text-default-white', clash.advantage === 'EVEN' && 'bg-default-dim2 text-default-black2', )} @@ -223,14 +223,14 @@ export default function ReportView(props: ReportViewProps) {
-
-

+

+

{t('찬성')}

{clash.prosPosition}

-
-

+

+

{t('반대')}

{clash.consPosition}

diff --git a/src/page/DebateTranscriptPage/components/TranscriptView.tsx b/src/page/DebateTranscriptPage/components/TranscriptView.tsx index 8566979b..c8aeed3e 100644 --- a/src/page/DebateTranscriptPage/components/TranscriptView.tsx +++ b/src/page/DebateTranscriptPage/components/TranscriptView.tsx @@ -12,8 +12,8 @@ interface TranscriptViewProps { } const STANCE_BORDER_CLASS: Record = { - PROS: 'border-camp-blue', - CONS: 'border-camp-red', + PROS: 'border-stance-blue', + CONS: 'border-stance-red', NEUTRAL: 'border-default-neutral', }; diff --git a/src/page/DebateTranscriptPage/stanceStyle.ts b/src/page/DebateTranscriptPage/stanceStyle.ts index 9540eb75..860d5d03 100644 --- a/src/page/DebateTranscriptPage/stanceStyle.ts +++ b/src/page/DebateTranscriptPage/stanceStyle.ts @@ -1,7 +1,7 @@ import { Stance } from '../../types/type'; export const STANCE_TEXT_CLASS: Record = { - PROS: 'text-camp-blue', - CONS: 'text-camp-red', + PROS: 'text-stance-blue', + CONS: 'text-stance-red', NEUTRAL: 'text-default-black2', }; diff --git a/src/page/TimerPage/components/CircularTimer.tsx b/src/page/TimerPage/components/CircularTimer.tsx index f57ada67..2fe99875 100644 --- a/src/page/TimerPage/components/CircularTimer.tsx +++ b/src/page/TimerPage/components/CircularTimer.tsx @@ -30,8 +30,8 @@ export default function CircularTimer({ : stance === 'NEUTRAL' ? '#A3A3A3' // bg-default-neutral : stance === 'PROS' - ? '#1E91D6' // bg-camp-blue - : '#E14666'; // bg-camp-red + ? '#1E91D6' // bg-stance-blue + : '#E14666'; // bg-stance-red const angle = useTransform(progress, [0, 100], [0, 360]); const backgroundImage = useTransform(angle, (currentAngle) => { if (currentAngle >= 360) { diff --git a/src/page/TimerPage/components/NormalTimer.tsx b/src/page/TimerPage/components/NormalTimer.tsx index 7e76e6c7..b378b927 100644 --- a/src/page/TimerPage/components/NormalTimer.tsx +++ b/src/page/TimerPage/components/NormalTimer.tsx @@ -114,11 +114,12 @@ export default function NormalTimer({ className={clsx( 'flex h-[52px] w-full items-center justify-center rounded-[20px] text-[20px] font-semibold transition-all duration-200 ease-in-out xl:h-[68px] xl:text-[28px]', { - 'bg-camp-blue/50 hover:bg-camp-blue': + 'bg-stance-blue/50 hover:bg-stance-blue': item.stance === 'PROS', }, { - 'bg-camp-red/50 hover:bg-camp-red': item.stance === 'CONS', + 'bg-stance-red/50 hover:bg-stance-red': + item.stance === 'CONS', }, { 'bg-default-neutral/50 hover:bg-default-neutral': diff --git a/src/page/TimerPage/components/TimeBasedTimer.tsx b/src/page/TimerPage/components/TimeBasedTimer.tsx index 8523430e..c1abc6d0 100644 --- a/src/page/TimerPage/components/TimeBasedTimer.tsx +++ b/src/page/TimerPage/components/TimeBasedTimer.tsx @@ -168,8 +168,8 @@ export default function TimeBasedTimer({

{t('현재 시간')} diff --git a/src/page/TimerPage/components/TimerController.tsx b/src/page/TimerPage/components/TimerController.tsx index 33a9b925..b9ca3653 100644 --- a/src/page/TimerPage/components/TimerController.tsx +++ b/src/page/TimerPage/components/TimerController.tsx @@ -27,9 +27,9 @@ export default function TimerController({ boxType === 'FEEDBACK' ? 'bg-brand' : stance === 'PROS' - ? 'bg-camp-blue' + ? 'bg-stance-blue' : stance === 'CONS' - ? 'bg-camp-red' + ? 'bg-stance-red' : 'bg-default-neutral'; return (
From 1c23713c4edb1757b6c46ea5ed61bd1086fa6650 Mon Sep 17 00:00:00 2001 From: Shawn Kang Date: Sat, 24 Oct 2026 20:26:22 +0900 Subject: [PATCH 2/2] =?UTF-8?q?docs:=20=EA=B3=B5=ED=86=B5=20=EB=94=94?= =?UTF-8?q?=EC=9E=90=EC=9D=B8=20=EC=BB=A8=EB=B2=A4=EC=85=98=EA=B3=BC=20UI?= =?UTF-8?q?=20=EC=9E=91=EC=84=B1=20=EC=A7=80=EC=B9=A8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/README.md | 43 +++++---- docs/conventions/design.md | 145 +++++++++++++++++++++++++++++++ docs/conventions/react-and-ui.md | 16 +++- 3 files changed, 184 insertions(+), 20 deletions(-) create mode 100644 docs/conventions/design.md diff --git a/docs/README.md b/docs/README.md index 9a4d9dbb..e188a2b4 100644 --- a/docs/README.md +++ b/docs/README.md @@ -70,12 +70,13 @@ CI·개발·미리보기·생산·Storybook의 Node 설정은 ## 4. 코드와 문서 컨벤션 -| 문서 | 소유하는 규칙 | -| ---------------------------------------------------------- | ------------------------------------------------ | -| [문서 작성](conventions/docs.md) | Markdown 형식, 길이, 문체, 중복, 링크·갱신 | -| [TypeScript·구조](conventions/typescript-and-structure.md) | 이름·선언·타입·파일 배치·주석 | -| [React·UI](conventions/react-and-ui.md) | 컴포넌트·Props·훅·effect·스타일·국제화·접근성 | -| [데이터·실시간](conventions/data-and-realtime.md) | Query·Mutation·API·MSW·Repository·소켓 작성 형식 | +| 문서 | 소유하는 규칙 | +| ---------------------------------------------------------- | -------------------------------------------------------- | +| [문서 작성](conventions/docs.md) | Markdown 형식, 길이, 문체, 중복, 링크·갱신 | +| [TypeScript·구조](conventions/typescript-and-structure.md) | 이름·선언·타입·파일 배치·주석 | +| [React·UI](conventions/react-and-ui.md) | 컴포넌트·Props·훅·effect·스타일·국제화·접근성 | +| [공통 디자인](conventions/design.md) | 시각 방향·색상·타이포그래피·형태·공간 대응·움직임 | +| [데이터·실시간](conventions/data-and-realtime.md) | Query·Mutation·API·MSW·Repository·소켓 작성 형식 | | [테스트·Storybook](conventions/testing-and-storybook.md) | TDD, 테스트·모킹·Story 작성, Storybook 10 설정·수동 검증 | ## 5. 스킬 워크플로 @@ -90,18 +91,19 @@ CI·개발·미리보기·생산·Storybook의 Node 설정은 모든 코드 변경 전에 [헌법](CONSTITUTION.md)과 이 문서를 읽고, 아래에서 해당하는 상세 문서를 함께 읽음. 여러 행에 해당하면 모두 적용함. -| 작업 | 읽을 상세 문서 | 계약 변경 시 갱신할 문서 | -| ---------------------------------- | --------------------------------------------- | --------------------------------------- | -| 페이지·공용 UI·상태 훅 | 구조와 UI, React·UI, TypeScript·구조 | 구조와 UI 또는 React·UI | -| 라우트·인증·언어 | 앱·라우팅·국제화, React·UI | 앱·라우팅·국제화 | -| HTTP·Query·Mutation·Repository·MSW | 데이터 접근, 데이터·실시간, 테스트·Storybook | 데이터 접근 또는 데이터·실시간 | -| 소켓·타이머 공유 상태 | 실시간 통신, 데이터·실시간, 테스트·Storybook | 실시간 통신 또는 데이터·실시간 | -| 오류 UI·Sentry | 오류와 관측, React·UI, 테스트·Storybook | 오류와 관측 | -| 테스트·Story | 테스트·Storybook, 대상 기능의 아키텍처·컨벤션 | 테스트·Storybook 또는 대상 문서 | -| 문서·템플릿 | 문서 작성, 이 문서, 관련 상세 문서 | 원칙 변경은 헌법, 계약 변경은 소유 문서 | -| 실행 환경·의존성·workflow | 실행 환경 설정, 테스트·Storybook, 문서 작성 | 실행 환경 설정 | -| 코드 리뷰·리뷰 보고서 | 코드 리뷰, 변경 영역의 상세 문서 | 코드 리뷰 또는 해당 상세 문서 | -| 일반 PR 준비·생성 | 일반 PR 생성, 코드 리뷰, 문서 작성 | 일반 PR 생성 또는 코드 리뷰 | +| 작업 | 읽을 상세 문서 | 계약 변경 시 갱신할 문서 | +| ---------------------------------- | ------------------------------------------------- | --------------------------------------- | +| 페이지·공용 UI·상태 훅 | 구조와 UI, React·UI, TypeScript·구조, 공통 디자인 | 구조와 UI, React·UI 또는 공통 디자인 | +| 디자인·타이포그래피 | 공통 디자인, React·UI, 문서 작성 | 공통 디자인 | +| 라우트·인증·언어 | 앱·라우팅·국제화, React·UI | 앱·라우팅·국제화 | +| HTTP·Query·Mutation·Repository·MSW | 데이터 접근, 데이터·실시간, 테스트·Storybook | 데이터 접근 또는 데이터·실시간 | +| 소켓·타이머 공유 상태 | 실시간 통신, 데이터·실시간, 테스트·Storybook | 실시간 통신 또는 데이터·실시간 | +| 오류 UI·Sentry | 오류와 관측, React·UI, 테스트·Storybook | 오류와 관측 | +| 테스트·Story | 테스트·Storybook, 대상 기능의 아키텍처·컨벤션 | 테스트·Storybook 또는 대상 문서 | +| 문서·템플릿 | 문서 작성, 이 문서, 관련 상세 문서 | 원칙 변경은 헌법, 계약 변경은 소유 문서 | +| 실행 환경·의존성·workflow | 실행 환경 설정, 테스트·Storybook, 문서 작성 | 실행 환경 설정 | +| 코드 리뷰·리뷰 보고서 | 코드 리뷰, 변경 영역의 상세 문서 | 코드 리뷰 또는 해당 상세 문서 | +| 일반 PR 준비·생성 | 일반 PR 생성, 코드 리뷰, 문서 작성 | 일반 PR 생성 또는 코드 리뷰 | 표의 항목은 2~5절의 설정 설명과 문서 링크를 가리킴. 정확히 일치하는 행이 없으면 변경 대상과 소비자를 기준으로 관련 문서를 선택함. 경로·기술·작업 분류가 바뀌면 이 문서의 @@ -114,3 +116,8 @@ CI·개발·미리보기·생산·Storybook의 Node 설정은 확정 규칙과 다를 수 있음. 기존 예외를 새 규칙으로 해석하지 않음. 공용 도메인 타입의 `src/types/` 이동은 이 개편에서 함께 수행함. 규칙과 코드 경로가 달라졌다면 현재 상태와 목표를 구분해 해당 상세 문서를 수정함. + +[공통 디자인](conventions/design.md)의 브랜드·진영색, 기본 팔레트 사용과 역할별 +타이포그래피 utility는 전역 CSS에 반영했음. 기존 사용자 정의 색상의 의미별 +전환과 기존 화면의 글자 역할 적용은 아직 목표 상태임. 현재 구현과의 차이와 +적용 경계는 해당 문서에서 확인함. diff --git a/docs/conventions/design.md b/docs/conventions/design.md new file mode 100644 index 00000000..ec1da49d --- /dev/null +++ b/docs/conventions/design.md @@ -0,0 +1,145 @@ +# 공통 디자인 컨벤션 + +## 적용 범위와 현재 구현 + +페이지·공용 UI·기능 전용 UI의 시각 기준을 소유함. 신규 UI와 실질적으로 변경하는 +UI는 이 기준에 맞춰 설계함. 같은 역할은 같은 기준을 사용하고, 사용 목적이 +다르면 필요한 변형을 허용함. 변형은 기본 기준과 다른 이유·적용 범위를 간단히 +기록하며, 화면마다 독립적인 시안을 만들지 않음. + +**현재 상태와 목표**: 브랜드·진영색, Tailwind CSS 4 기본 팔레트 사용과 아래 +타이포그래피의 공용 utility를 전역 CSS에 반영했음. 기존 사용자 정의 색상의 +의미별 전환과 기존 화면의 글자 역할 적용은 아직 UI 전체에 반영되지 않은 목표임. +후속 UI 구현에서 실제 CSS 정의와 사용처를 함께 확인해 반영함. 기존 구현의 +차이를 새로운 디자인의 예외로 해석하지 않음. + +UI 책임과 재사용 범위는 [구조와 UI](../architecture/structure-and-ui.md), +스타일 구현·접근성·사용자 문구는 [React·UI](react-and-ui.md), 테스트와 시각 +검증 절차는 [테스트·Storybook](testing-and-storybook.md)이 소유함. + +## 기본 방향과 변형 + +흰 바탕·어두운 글자·노랑 강조를 기본으로 밝고 명확한 화면을 구성함. 현재의 +[시간표 편집](../../src/page/TableComposition/TableCompositionPage.tsx)· +[개요](../../src/page/TableOverviewPage/TableOverviewPage.tsx)가 기본 인상의 +근거임. 해당 화면의 모든 치수·배치·상태 표현을 규칙으로 채택하는 것은 아님. + +일반 UI는 여유 있는 간격과 충분한 조작 영역을 기본으로 함. 정보량이 많은 곳은 +조밀한 변형을 허용하되, 글자와 조작 영역을 무리하게 줄여 밀도를 높이지 않음. +원거리에서 확인하는 숫자, 긴 글을 읽는 영역, 소개용 큰 문구처럼 목적이 다른 +표현은 그 목적에 맞게 조정함. 변형을 위한 크기 종류를 미리 늘리지 않음. + +## 색상과 강조 + +### 유지하는 브랜드·진영색 + +[index.css](../../src/index.css)에 정의된 다음 색상을 유지함. + +| 역할 | 이름 | 값 | +| ----------- | ------------- | --------- | +| 브랜드 강조 | `brand` | `#FECD4C` | +| 브랜드 호버 | `brand-hover` | `#DBA822` | +| 찬성 진영 | `stance-blue` | `#1E91D6` | +| 반대 진영 | `stance-red` | `#E14666` | + +### 중립·오류색의 목표 + +중립색은 Tailwind CSS 4의 `neutral`, 오류색은 `red` 기본 팔레트를 사용함. +기존 v3 호환 재정의값을 목표 색상으로 고정하지 않음. 배경·본문·보조 설명· +경계·비활성 등 의미에 맞는 단계를 사용하고, 모든 단계의 사용처를 미리 정하지 +않음. 흰색은 기본 면으로 사용하며 회색 계열을 화면마다 따로 선택하지 않음. + +일반적인 주요 행동과 선택 강조는 브랜드 노랑을 중심으로 구성함. 컴포넌트마다 +보라·파랑 등 별도의 일반 선택색을 만들지 않음. 진영과 오류처럼 별도 의미가 +있는 색은 해당 역할에 사용하며, 색만으로 상태를 전달하지 않음. 비활성·미선택· +호버는 서로 다른 상태로 구분함. + +색상 단계는 실제 배경과 글자·아이콘의 조합에서 읽기 쉬운지 확인해 선택함. +현재 `default-*`·`semantic-*` 색상이나 기본 팔레트 이름을 사용하는 것만으로 +새 기준을 충족했다고 간주하지 않음. 기본 팔레트의 v3 호환 재정의는 제거했으나 +기존 UI가 소비하는 사용자 정의 색상은 남아 있으므로 이름과 실제 값을 함께 +확인해야 함. + +## 타이포그래피 + +### 역할별 기본 스타일 + +글꼴은 현재 전역 Pretendard 계열과 대체 글꼴 순서를 상속함. 자간은 기본값을 +사용함. 글자 크기·굵기·행간을 하나의 역할별 스타일로 다루고, 색상은 강조· +오류 등 문맥에 따라 별도로 선택함. + +| 역할 | 용도 | 클래스 | 크기 | 굵기 | 행간 | +| -------- | --------------------- | ------------------ | ---- | ---- | ---- | +| Headline | 화면의 대표 제목 | `text-ui-headline` | 28px | 700 | 36px | +| Title | 구역·내용 묶음의 제목 | `text-ui-title` | 24px | 600 | 32px | +| Body | 본문 | `text-ui-body` | 16px | 400 | 24px | +| Label | 짧은 조작·항목 이름 | `text-ui-label` | 16px | 600 | 24px | +| Caption | 보조 설명 | `text-ui-caption` | 14px | 400 | 20px | + +현재 전역 글자 크기를 출발점으로 삼고 보조 설명에 필요한 크기를 보완했음. +제목은 크기와 굵기로 분명히 드러내며, 본문과 보조 설명은 읽기 편한 차이를 둠. +역할마다 기본 스타일 하나로 시작하고 실제 필요가 있을 때만 변형을 추가함. +모든 역할에 작은·중간·큰 단계를 만들지 않음. + +### 역할 선택과 기존 선택자의 차이 + +화면 대표 제목에는 Headline, 그 아래 구역 제목에는 Title을 선택함. 본문은 +Body, 버튼·항목의 짧은 이름은 Label, 부가 설명은 Caption을 기본으로 함. +색이나 크기만 보고 역할을 선택하지 않음. + +시각 역할을 HTML 요소와 일대일로 연결하지 않음. 예를 들어 Title 스타일의 +HTML 제목 수준은 문서 구조에 따라 결정하며, Label 스타일이 HTML `label` +요소만을 뜻하지 않음. 의미 구조는 [React·UI](react-and-ui.md)의 계약을 따름. + +현재 `.text-title`은 28px 크기만 선언하며 위 Title 역할의 전체 스타일과 다름. +기존 `.text-body` 등도 굵기·행간을 포함하는 새 역할의 구현으로 간주하지 않음. +새 역할에는 위 표의 `text-ui-*`를 사용함. 타이머 수치나 홍보 문구의 변형도 +일반 역할의 기본값을 바꾸는 근거로 삼지 않음. + +### 사용과 사양 동기화 + +[index.css](../../src/index.css)의 `@theme`에 `--text-ui-{역할}`과 이에 대응하는 +`--line-height`·`--font-weight` 속성을 정의함. Tailwind가 이를 묶어 위 클래스와 +`md:text-ui-title` 같은 화면 조건 조합을 생성함. 글꼴·색·자간은 역할에 고정하지 +않으며, 선택한 역할의 크기·굵기·행간을 개별 utility로 임의 덮어쓰지 않음. +변형이 필요하면 이 문서의 공통 변형 원칙을 적용함. + +```tsx +

{t('토론 기록')}

+

{description}

+``` + +**타이포그래피 사양을 변경할 때는 이 문서의 표와 `src/index.css`의 대응 +`@theme` 정의를 반드시 같은 변경에서 갱신함.** 역할·클래스명을 추가·변경·삭제하면 +사용처와 [디자인 샘플 Story](../../src/components/DesignSystemSample/DesignSystemSample.stories.tsx)도 +함께 갱신함. 값만 바뀌는 경우 Story는 같은 클래스를 통해 변경값을 반영함. +문서의 사양과 실제 생성 CSS의 크기·굵기·행간을 대조하고 lint·build로 검증함. + +## 형태·영역 구분·행동의 위계 + +둥근 인상을 유지하되 작은 요소와 큰 면의 모서리를 소수 단계로 구분함. +완전히 둥근 형태는 강조·조작 등 역할이 분명한 곳에 사용함. 컴포넌트마다 +독립적인 곡률을 만들거나 모든 요소를 같은 곡률로 맞추지 않음. + +버튼은 공통 글자·형태·상태 표현 체계 안에서 행동의 중요도와 의미에 따라 +강조 수준을 구분함. 주요 행동은 브랜드 강조, 보조 행동은 약한 면·테두리 등으로 +위계를 만듦. CTA와 확인·취소라는 이름만으로 서로 독립적인 시안을 만들지 않음. +확인 동작도 해당 맥락의 주요 행동이면 같은 주요 강조 원칙을 사용함. + +영역 구분과 상호작용 강조는 배경·테두리 변화를 우선함. 그림자는 떠 있는 면 등 +깊이가 필요한 경우에 사용함. 확대·색 발광은 일반 강조 수단으로 널리 사용하지 +않고 목적이 분명한 경우에 제한함. 각 컴포넌트의 치수·곡률·그림자 수치를 모두 +공통 지침으로 고정하지 않음. + +## 공간 제약과 움직임 + +화면이 좁거나 문구가 길면 정보와 조작의 가독성을 우선함. 줄바꿈·재배치·영역 +확장·필요한 스크롤로 대응하고 한 화면에 넣기 위해 글자·조작 영역을 무리하게 +축소하지 않음. 요약 영역에서 말줄임을 사용하면 펼침·상세 이동 등 전체 내용을 +확인할 경로를 둠. 한국어·영어의 길이 차이와 좁거나 낮은 화면도 함께 확인함. +화면 조건의 정의는 [React·UI](react-and-ui.md#스타일과-접근성)를 따름. + +움직임은 상태 변화와 동작 결과를 이해시키는 짧은 전환을 기본으로 함. 장식적· +반복적 움직임은 제한하고, 필요한 진행 표시에는 움직임 감소 설정에서 의미를 +유지하는 대체 표현을 제공함. 모든 컴포넌트의 전환 시간을 하나의 수치로 +고정하지 않음. diff --git a/docs/conventions/react-and-ui.md b/docs/conventions/react-and-ui.md index 014d8d90..76617bd6 100644 --- a/docs/conventions/react-and-ui.md +++ b/docs/conventions/react-and-ui.md @@ -40,6 +40,12 @@ effect 밖에서도 호출해야 하는 콜백은 기존 함수·`useCallback`· ## 스타일과 접근성 +새 UI는 기존 UI의 재사용 가능성을 먼저 확인하고, +[공통 디자인](design.md)에서 시각 역할·필요한 변형을 선택함. 구현 시 실제 CSS +정의와 사용처를 대조하고, 관련 상태·긴 번역 문구·화면 조건에서 결과를 확인함. +UI 책임과 재사용 범위는 [구조와 UI](../architecture/structure-and-ui.md), +검증 절차는 [테스트·Storybook](testing-and-storybook.md)을 따름. + Tailwind utility와 제한된 공통 CSS를 사용함. `className` 문자열이 100자를 초과하면 `UPPER_SNAKE_CASE` 상수로 분리함. `DT*` 아이콘은 해당 아이콘 컴포넌트 파일에서 직접 import함. 다른 아이콘의 export 방식은 규정하지 않음. @@ -55,8 +61,14 @@ utility를 사용하며 일반 CSS 선택자를 참조하지 않음. 기존 `.bu 화면 조건은 `md` 768px 이상, `lg` 1280px 이상임. `xl`은 너비 1600px 이상이면서 높이 1024px 이상, `short`는 높이 500px 이하인 사용자 정의 variant임. 기본 breakpoint는 초기화하며 `sm` 등 다른 화면 조건을 자동으로 추가하지 않음. -프로젝트 색상과 사용 중인 기본 팔레트 색상은 v3 값을 유지함. 색상 추가 시 -기존 디자인과의 호환 여부를 확인함. +기본 팔레트는 v3 호환 재정의 없이 Tailwind CSS 4의 내장값을 사용함. 브랜드· +진영색과 기존 UI가 소비하는 `default-*`·`semantic-*` 사용자 정의 색상은 +`src/index.css`에 남아 있음. 색상의 의미별 사용 기준은 +[공통 디자인](design.md#색상과-강조)이 소유하며, 기존 사용자 정의 색상을 +전환할 때는 사용처를 함께 확인함. 새 글자 역할은 +[역할별 타이포그래피](design.md#타이포그래피)의 `text-ui-*`를 사용하며, 기존의 +크기 전용 글자 스타일 선택자와 구분함. 사양과 소스의 동기화는 +[사용과 사양 동기화](design.md#사용과-사양-동기화)를 따름. 클래스 정렬은 `.prettierrc`의 `prettier-plugin-tailwindcss`가 담당함. `tailwindStylesheet`는 `./src/index.css`, `tailwindFunctions`는 `['clsx']`임.