diff --git a/CLAUDE.md b/CLAUDE.md index 02fca7b..bc16379 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -82,6 +82,8 @@ GUARDiA 표준 프레임워크(Spring Boot 3.5 + React 19 + MyBatis + PostgreSQL | 2026-07-11 | 구현 하네스 초기 구성 — 전문 에이전트 6종 + kintex-impl-orchestrator + 구현 백로그 | 전체 | 범용 에이전트만 존재 → 스택 확정 후 실제 구현 조율 팀 구성 | | 2026-07-11 | **v2.0 재구성** — 자동전시시스템 확장(PLANNING v2.0). 신규 에이전트 11종(아키텍트 5·공통 1·도메인 5·AI 1) + 오케스트레이터·백로그 v2.0(Phase A 아키텍처→B 공통레이어→C 부스코어→D 도메인→E 배포) | 전체 | 스코프 확장(전시 전기능·경영분석·CMS·관리자·역할분리·공사 옥션·WISE 공통레이어·AI Claude 전환) | | 2026-07-11 | WISE 웹 기능 레퍼런스 강화 — kintex-frontend-dev에 uiws frontend(pages 공통 업무기능) 차용 원칙, 오케스트레이터에 웹 기능 화면 WISE 패턴·Stitch 디자인 경유·모바일 경계 명시 + 로스터에 kintex-mobile-dev(경계) 추가 | kintex-frontend-dev·kintex-impl-orchestrator | "웹도 wise 기능 참고" 요청 | +| 2026-07-12 | **세션 대량 구현·배포** — visitor 메인(AI 관람 도우미 홈·기능타일·연월 브리핑, 근거=크롤 적재 DB)·3-트랙 관문/서브홈·역할별 랜딩+메뉴 게이트(plan_role_routing)·공개 AI/캘린더 API(V43)·정산 데이터 정렬(V47)·포스터 이미지 빌드내장(V46)·성능 인덱스(V44)·아바타 API(V45). AI 토큰최소화 기획(PLANNING v3.4 §8A)·데이터표준(FK최소화·공통코드·view/mview·배치, data.md)·Open SSO+HR 연동 설계(sso-hr-integration.md). 배포 커밋 214b6df→3077cb0, health 게이트 통과 | 전체 | 소유자 라이브 피드백 연속 반영. **전 지시 로그: `docs/OWNER_FEEDBACK.md`(권위)** | +| 2026-07-12 | **선행 게이트 해소·검증 교훈** — G1 나노바나나 Gemini 라이브·G2 kintex.zioinfo.co.kr:8021 CI/CD 라이브. 배포 전 필수: **Flyway 마이그레이션 라이브 dry-run(BEGIN…ROLLBACK)**·시크릿 fail-fast 프로파일 확인·파일단위 커밋(공유 트리 교차 방지)·health 게이트. Stitch 불안정 시 design.md 스펙 직접 구현(문서화 폴백) | 전체·kintex-devops-dev | 다수 배포 롤백/캐시/교차커밋 사고 후 표준화 | --- @@ -95,6 +97,7 @@ GUARDiA 표준 프레임워크(Spring Boot 3.5 + React 19 + MyBatis + PostgreSQL | 날짜 | 변경 내용 | 대상 | 사유 | |------|----------|------|------| | 2026-07-12 | 초기 구성(analyst·crawler 에이전트 + 오케스트레이터) | 전체 | "크롤링/벤치마킹 하네스 생성" 요청 — COEX 3-트랙 재구성·행사 크롤 정례화 | +| 2026-07-12 | **모바일 앱 벤치마킹**(전시·공연 앱 다운로드·평점 상위순) → `docs/analysis/mobile-benchmark-exhibition-performance.md` + 백로그 12항목. 핵심: 다운로드↔만족도 상반(티켓팅 빅3 저평점) → MMCA·Eventbrite·DICE·Fever 롤모델. **관람객 정보 크롤→DB 적재**(V43 visitor_guide·transport, 포스터 34장 V46) | kintex-benchmark-analyst·kintex-crawler-dev | "모바일은 전시·공연 앱 벤치마킹 다운로드·좋아요순" + "크롤링해서 db 저장" | --- @@ -108,6 +111,7 @@ GUARDiA 표준 프레임워크(Spring Boot 3.5 + React 19 + MyBatis + PostgreSQL | 날짜 | 변경 내용 | 대상 | 사유 | |------|----------|------|------| | 2026-07-12 | 초기 구성(kintex-testdata-dev 신규 + wise-ui/menu-ia/qa 재사용 + 오케스트레이터) | 전체 | 소유자 지시 — 상세→메인 복귀 부재·전면 리뉴얼·테스트 데이터/공통코드/프로그램 채움 | +| 2026-07-12 | **코엑스풍 공개사이트 리뉴얼**(design.md v2.4 §3C-V 비주얼·모션 — 시네마틱 히어로·스크롤 리빌·카운트업·포스터 카드 리프트/줌·오토스크롤, prefers-reduced-motion·CLS 0) + **행사스코프 데이터 정렬**(V47 e-2026-live 폐루프·V49 잔여 보강). ★교훈: 화면이 비는 진짜 원인=시드가 데모 해소행사(workspaces[0]=start_date 최소)에 안 묶임 | kintex-frontend-dev·kintex-testdata-dev·designer | "코엑스처럼 지금 착수"·"정산 데이터 없음"·"모든 테이블 데이터" | --- @@ -122,6 +126,7 @@ GUARDiA 표준 프레임워크(Spring Boot 3.5 + React 19 + MyBatis + PostgreSQL |------|----------|------|------| | 2026-07-12 | 초기 구성(에이전트 2 신규 + kintex-qa 재사용 + 오케스트레이터) | 전체 | 소유자 강한 피드백("전부 WISE대로 안 되어 있다" — 시스템관리 메뉴 실종·햄버거/아코디언 부재·캘린더·대시보드·로고) | | 2026-07-12 | 메뉴 IA 전담 추가 — kintex-menu-ia-dev + kintex-menu-recompose 스킬(표준 카테고리 v1·라우트↔메뉴 정합 절차), 오케스트레이터 Phase 2.5 편입 | agents·skills | "카테고리별 메뉴 재구성 하네스" 요청 — 별도 오케스트레이터 대신 중복 회피 확장 | +| 2026-07-12 | **Nifty 디자인 시스템 전면 정렬** — 카드 Nifty 타입(`.kx-card` subtle 그림자·hover·`--flush`/`__media`/`__header`/`__body`/`__footer` 구조)·타이포(`--fs-micro`/`--fs-nano`·`--fw-*` 토큰·공용 `.kx-page__title`/`.kx-section__title`·font-size 179+weight 415 토큰화)·그리드(`.kx-table` Nifty Advanced header로 6화면 수렴). **캘린더 Nifty**(옅은 격자·연노랑 today·소프트 이벤트칩)·**파란버튼 흰글자**(`--color-on-accent` 다크대응)·favicon 정정. 기준: `docs/DESIGN_SYSTEM_NIFTY.md`·`_workspace/audit_typography_grid.md` | kintex-frontend-dev·kintex-qa | 소유자 반복 강피드백("Nifty 카드/그리드/폰트 안 맞음"·"파란버튼 흰글자"·"달력 Nifty") | --- @@ -138,3 +143,4 @@ GUARDiA 표준 프레임워크(Spring Boot 3.5 + React 19 + MyBatis + PostgreSQL |------|----------|------|------| | 2026-07-11 | 초기 구성 — kintex-mobile-dev 신규 + 오케스트레이터(재사용: designer·backend-dev·qa·devops-dev·reviewer). WISE 모바일 레퍼런스·Stitch 디자인 경유 반영 | 전체 | 모바일 앱 트랙 전담 하네스 부재("하네스 생성" + "wise 모바일 참고" + "디자인은 스티치" 요청) | | 2026-07-11 | **앱 타깃 2개 확정 반영** — 코드베이스 1(mobile/) + 배포 타깃 2(①운영 B2B: 2FA 필수·사내 QR ②관람객 B2C: 스토어 공개·간편가입·게스트). 계정은 단일 통합+가입 트랙 분리(PLANNING v3.1) | kintex-mobile-dev·오케스트레이터 | 소유자 확정 — 회원가입 관람객 포함 질의 → 하이브리드(계정 통합·앱 분리) 채택 | +| 2026-07-12 | **내정보(WISE)+생체인식(expo-local-authentication)+프로필 사진(expo-image-picker)** + **앱 위변조 방지·시큐어코딩 보안 체크리스트**(루트/탈옥·무결성·Hermes+R8·화면캡처·cleartext·권한최소, `docs/security/mobile-security-*.md`) + **다국어(react-i18next)·역할별 랜딩 패리티** 착수 | kintex-mobile-dev·kintex-qa | "모바일 내정보 WISE+생체+사진"·"앱 위변조방지·시큐어코딩·보안체크"·"모바일도 동일 로직"·"웹 4개국어인데 모바일은?" | diff --git a/docs/OWNER_FEEDBACK.md b/docs/OWNER_FEEDBACK.md new file mode 100644 index 0000000..f68e6f5 --- /dev/null +++ b/docs/OWNER_FEEDBACK.md @@ -0,0 +1,184 @@ +# 소유자 피드백·지시 로그 (KINTEX AI 전시·행사시스템) + +> 이번 세션에서 소유자가 지시한 모든 항목을 전수 기록한다. 상태: ✅완료(배포) · 🚚배포중 · ⏳작업중 · 📋대기. +> 이 파일을 기준으로 누락 없이 처리한다. 새 지시는 여기에 추가한다. +> 최종 갱신: 2026-07-12 + +--- + +## 1. 디자인 시스템 (Nifty / WISE) — 최우선 반복 지시 + +| # | 지시 | 상태 | 비고 | +|---|------|------|------| +| 1.1 | **Nifty 카드 타입** 적용 (https://preview.themeon.net/nifty/ui-elements/cards/) — 라운딩·그림자·헤더/바디/푸터 구조·보더 톤 | 📋대기 | `.kxp-ecard`는 8px 라운딩 있으나 Nifty 카드 구조 전면 정렬 필요. 다음 패스 | +| 1.2 | **그리드는 Nifty Advanced table headers**로 통일 (tables/static-tables) | ⏳작업중 | 정본 `.kx-table` 존재, 미준수 6화면 수렴 (감사완료) | +| 1.3 | **Tabulator 기능** 추가 (tables/tabulator — 정렬·필터·페이지·CSV) | 📋대기 | task #32 | +| 1.4 | **상세페이지 = Nifty blog** 스타일 (blog-apps/blog) | 📋대기 | task #33 | +| 1.5 | **버튼 = Nifty buttons**, **Dropdowns**, **컴포넌트**, **List Group**, **Typography/Modals/Offcanvas** = ui-elements 전체 채택 | ⏳부분 | Offcanvas 완료, 나머지 정렬 진행 | +| 1.6 | **Nifty 스타일 학습 md 생성** | ✅완료 | `docs/DESIGN_SYSTEM_NIFTY.md`·`docs/analysis/nifty-design-refs.md` | +| 1.7 | **아이콘은 선(line/stroke) SVG**로 직접 (이모지 금지) | ✅원칙 | 전 신규화면 준수 | +| 1.8 | **UI는 WISE(UIWS) 문자 그대로** — 셸·메뉴·캘린더 자체 재해석 금지 | ✅원칙 | 메모리 [[ui-follow-wise-strictly]] | +| 1.9 | **폰트를 페이지마다 안 맞춤** — 전 화면 타이포 일관화 | ⏳작업중 | 감사완료(하드코딩 262건/36파일). `--fs-micro`·공용 `.kx-page__title`·weight 토큰 신설로 정렬. 다음 패스 | +| 1.10 | **모든 경계선 엷은 회색** | ⏳부분 | 구조 보더는 neutral-200(#e4e7ec) 사용 중. 의미색·hover는 유지, 지정화면 정렬 | +| 1.11 | **좌우 2단 폭 50:50**, 전 화면 **공백 없는 반응형**(풀블리드) | ✅원칙 | task #39 스윕 대기 | +| 1.12 | **top 설정 아이콘** 추가 + **설정 레이아웃 커스터마이저**(offcanvas 테마/레이아웃/색상) | ⏳부분 | 설정 아이콘 완료, 커스터마이저 task #43 | + +## 2. 캘린더 + +| # | 지시 | 상태 | +|---|------|------| +| 2.1 | 캘린더를 **Nifty app-views/calendar**처럼 (옅은 회색 격자·연한 today·가벼운 이벤트 칩) | 🚚배포중 | 큰 캘린더 재스타일 완료(격자 neutral-100·today 연노랑·소프트 칩), 배포 중 | +| 2.2 | **공휴일 붉은색 + 일자 옆 공휴일명** (2050년까지 데이터) | 🚚배포중 | 큰 캘린더+미니 달력 배선. 7월은 공휴일 없음(8/15·9월 추석 표시) | +| 2.3 | **달력 경계선 모두 회색** | 🚚배포중 | 미니 달력 셀 회색 테두리 추가 | +| 2.4 | 일정 **월/주/일** 뷰 (WISE CalendarView) | ✅완료 | +| 2.5 | 전시 일정 50:50 비율·달력 날짜 위로(하단 공백 제거) | ✅완료 | + +## 3. 공개사이트 / 메인 / visitor + +| # | 지시 | 상태 | +|---|------|------| +| 3.1 | **첫 화면 = 제품 소개** (미인증 루트는 히어로 랜딩, 로그인 폼만 노출 금지) | ✅완료 | 메모리 [[product-identity-first]] | +| 3.2 | 제품명 **'KINTEX AI 전시·행사시스템'** | ✅완료 | +| 3.3 | **visitor 메인 = 세련된 홈페이지** (AI 관람 도우미 중심) | 🚚배포중 | AiAssistant·기능타일·연월 브리핑·쇼케이스 구현 | +| 3.4 | 메인에 **AI 기능 전면 노출** (AI가 뭘 하는지 한눈에) | 🚚배포중 | AI 기능 쇼케이스 6타일 | +| 3.5 | AI가 답하는 정보는 **크롤링해서 DB 저장** (실시간 외부호출 X) | ✅완료 | V43 visitor_guide·transport, 공개 AI API 근거응답 | +| 3.6 | **AI는 전시·행사정보 학습** (근거 기반) | ✅완료 | grounding + 인용, 환각 차단 | +| 3.7 | **AI가 연간·월간 전시·행사 기획정보 제시** | 🚚배포중 | AiPlanningBriefing(연/월 토글) | +| 3.8 | 비지터가 **가장 알고 싶은 정보를 AI로 쉽게** | ✅설계 | 관람 도우미 예시칩·구조화 카드 | +| 3.9 | 코엑스 분석 → **visitor/business/agency 3-트랙** 분리 | ✅완료 | 관문 + 3 서브홈 + TrackSwitcher | +| 3.10 | **"코엑스처럼" 전면 리뉴얼** (히어로 배경·애니메이션·비주얼) | 🚚배포중 | design.md v2.4 §3C-V, 코엑스풍 비주얼·모션 구현 | +| 3.11 | 메인 카드가 **밋밋한 파란 placeholder** (포스터 이미지 없음) | ✅완료 | V46 실제 포스터 이미지로 교체(배포됨) | +| 3.12 | **메인 카드 이미지 크기 안 맞음**(세로 포스터가 16:11 가로 프레임에 늘어남/잘림) | ⏳수정됨 | 포스터 실측 세로 0.67~0.8 → 카드 미디어 3:4 프레임 + `.kxp-ecard__poster` `object-fit:cover`(왜곡 제거). 다음 배포 | +| 3.13 | **메인 히어로 이미지 5장 회전(슬라이드)** | ⏳착수 | 크롤러가 KINTEX 전시장/시설 히어로 이미지 5장 `public/media/hero/` 확보 → 프론트 히어로 회전 UI(배포 후) | + +## 4. 역할 · 네비게이션 + +| # | 지시 | 상태 | +|---|------|------| +| 4.1 | **로그인 사용자 구분으로 메인페이지 + 메뉴 그룹 결정** (디폴트 visitor, 로그인 후 business/admin/agency) | 🚚배포중 | plan_role_routing 구현(admin→/admin·agency→/contractor·visitor→/visitor·ops/business→/home) + 역할별 메뉴 게이트 | +| 4.2 | **각 트랙 메인페이지 Stitch 디자인 의뢰** | ✅부분 | Stitch 2회 실패 → design.md 스펙 직접 구현(문서화 폴백) | +| 4.3 | **모바일 앱도 동일 로직** | 📋대기 | 웹 완료 후 모바일 반영 | +| 4.4 | **상세→메인 복귀·브레드크럼·404** 내비게이션 | ⏳ | renewal 하네스 | + +## 5. 데이터 / 시드 + +| # | 지시 | 상태 | +|---|------|------| +| 5.1 | **모든 테이블에 데이터** (50~100건) | ✅완료 | V42 대량시드(40테이블 2,535행) | +| 5.2 | **정산·결제 데이터 없음** — 행사스코프 정렬 | 🚚배포중 | V47: 데모 해소행사 `e-2026-live`에 정산 폐루프+옥션+관람객+리드+캠페인 | +| 5.3 | 테스트 데이터 **더 현실적으로** | ✅완료 | V39·V42 실감형 | +| 5.4 | 행사정보 **크롤링해서 데이터 넣기** | ✅완료 | 크롤 시드 | +| 5.5 | **연간·월간 전시 기획정보**도 적재 | ✅완료 | V43 event 생성열·v_event_calendar 뷰 | +| 5.6 | 포스터 이미지 **다 다운받아 빌드 내장** | 🚚배포중 | V46 34장 `/media/posters/` | + +## 6. 정산·도메인 화면 + +| # | 지시 | 상태 | +|---|------|------| +| 6.1 | **정산·결제 패딩 없음** | 🚚배포중 | `.kx-settle` 패딩 추가 | +| 6.2 | **참가업체 관리가 홈으로 감** — 전용 화면 신설 | 📋대기 | task #41 | +| 6.3 | 부서 관리 = **WISE 조직도** | ✅완료 | +| 6.4 | 결재관리 **반려·재상신·결재선** (WISE) | ✅완료 | +| 6.5 | 통합검색에 **행사·전시 포함** | ✅완료 | +| 6.6 | 시스템관리 **메시지 관리**({} 플레이스홀더+샘플) | 📋대기 | task #35 | +| 6.7 | 물류허브·서류/마일스톤·CMS·일정 = WISE | ✅완료 | +| 6.8 | **그룹권한(역할↔메뉴) 메뉴** 존재 확인 | ✅확인 | RoleAdminPage + RoleMenuAdminPage | + +## 7. 버튼 · 색상 · 대비 + +| # | 지시 | 상태 | +|---|------|------| +| 7.1 | **파란 배경 버튼 = 흰 글자** | ✅완료 | `--color-on-accent`(다크에서도 흰색). 라이브 반영(캐시 새로고침 필요) | +| 7.2 | 다크모드 색상배경 위 **흰 글자 어두워지는 버그** 전수 수정 | ✅완료 | 77곳 on-accent. +통계밴드 2곳 추가수정(배포중) | +| 7.3 | 회색 버튼은 옅은 ghost — 흰 글자 아님(솔리드 진회색만 흰글자) | ✅확인 | + +## 8. 파비콘 · 브랜드 + +| # | 지시 | 상태 | +|---|------|------| +| 8.1 | **KINTEX favicon** 추가 (홈페이지 크롤) | ✅완료 | 리포 favicon.ico가 톰캣 아이콘이던 것 규명→KINTEX .ico 재생성(v3) | +| 8.2 | **브랜드 밑에 영어 병기** | ✅완료 | "AI Exhibition & Event System" | + +## 9. 모바일 + +| # | 지시 | 상태 | +|---|------|------| +| 9.1 | **내정보 (WISE 참조) + 생체인식 + 사진등록** | ✅완료 | app/profile, expo-local-authentication, expo-image-picker. 아바타 API(백엔드) 완료 | +| 9.2 | **전시·공연 앱 벤치마킹** (다운로드·좋아요 상위순) | ✅완료 | docs/analysis/mobile-benchmark-exhibition-performance.md + 백로그 12항목 | +| 9.3 | **앱 위변조 방지 + 시큐어 코딩 + 보안 체크리스트 통과** | ✅완료 | 루트/탈옥·무결성·Hermes+R8·화면캡처·cleartext·권한최소. 체크리스트/감사 문서 | +| 9.4 | **다국어** — 웹 4개국어 완비, 모바일은 한국어(백로그) | ⏳ | 웹 ✅(ko/en/zh/ja), 모바일 i18n 미도입(MB-11) | + +## 10. AI + +| # | 지시 | 상태 | +|---|------|------| +| 10.1 | **AI 기능 쉽게 사용 + 토큰 최소화** | ✅완료 | PLANNING v3.4 §8A(결정론 라우팅·소형모델 우선·RAG축소·캐싱·집계는 SQL·측정지표) | +| 10.2 | AI 기능이 **하나도 구현 안 된 것 같다 → 구현** | ✅완료 | 공개 AI 관람 도우미 라이브(grounded=true, Claude) | +| 10.3 | 나노바나나 유료? | ✅답변 | Google Gemini 유료 | + +## 11. 데이터 아키텍처 (표준) + +| # | 지시 | 상태 | +|---|------|------| +| 11.1 | **FK 최소화, 공통코드로 관리** | ✅표준 | docs/architecture/data.md §10 (소프트참조·공통코드) | +| 11.2 | **필요한 인덱스 생성** | 🚚배포중 | V44 성능 인덱스 8건(핫패스) — 이미 배포됨 | +| 11.3 | **view/mview/function 고려** | ✅표준 | data.md §11 | +| 11.4 | **자동화 배치·프로시저 고려** | ✅표준 | data.md §13 배치 카탈로그 | +| 11.5 | **ten_id/tenant_id PK 맨 앞 복합키** | ✅원칙 | 메모리 [[tenant-id-pk-standard]] | + +## 12. 인증 / SSO / HR + +| # | 지시 | 상태 | +|---|------|------| +| 12.1 | **직원 인사(조직) 정보는 API 방식** | ✅설계 | docs/architecture/sso-hr-integration.md (HrDirectoryClient 어댑터+스냅샷) | +| 12.2 | **Open SSO 도입** | ✅설계 | Keycloak OIDC(PKCE)+SAML, 기존 JWT+2FA 공존(브로커) | + +## 13. 평면도 / 기타 + +| # | 지시 | 상태 | +|---|------|------| +| 13.1 | 도면 안 열림 / AI 시각화 어디감 | ✅원인 | 403(행사 미가입) → 데모계정 멤버십 해소. DWG는 SVG캔버스+JPG 렌더(DWG 바이너리는 좌표소스) | +| 13.2 | ReRoomAI 기능 (나노바나나 image-to-image) | ⏳ | visualizer 트랙 | + +--- + +## 미해결·대기 요약 (다음 우선 처리) + +1. **Nifty 디자인 시스템 전면 정렬** — 카드(ui-elements/cards)·폰트(--fs-micro·공용 제목클래스)·그리드(.kx-table 6화면) 일괄 (1.1·1.2·1.9) +2. **참가업체 관리 전용 화면** (6.2 / task #41) +3. **메시지 관리** (6.6 / task #35) +4. **Tabulator**·**상세 Nifty blog**·**설정 커스터마이저** (1.3·1.4·1.12 / task #32·33·43) +5. **전 화면 공백없는 반응형 스윕** (1.11 / task #39) +6. **모바일 역할 랜딩·다국어** (4.3·9.4) +7. **상세→메인 내비게이션** (4.4) + +--- + +## 처리 로그 (내가 처리·배포한 것 — 2026-07-12 세션) + +**배포 완료 (라이브):** 커밋 214b6df → 6e95ac6 → 87f310f/55e27b3 → 3077cb0 +- V42 대량시드(40테이블 2,535행) · V43 관람객 가이드/연월 캘린더 뷰 · V44 성능 인덱스 8건 · V45 아바타 컬럼 · V46 포스터 로컬화(73건) · V47 e-2026-live 정산 폐루프 +- 공개 AI 관람 도우미 API(grounded·Claude) · 연월 캘린더 API · 3-트랙 관문/서브홈 · 역할별 랜딩+메뉴 게이트 +- 미니/큰 캘린더 공휴일 표시·회색 테두리·Nifty 톤 · 코엑스풍 공개사이트 비주얼(히어로·모션·통계밴드) · 실제 포스터 이미지 +- 파란버튼 흰글자(`--color-on-accent`) · 다크모드 흰글자 77곳 · favicon(톰캣→KINTEX v3) · 2FA 방패 아이콘 · 브랜드 영문병기 · 정산 패딩 + +**구현 완료 (미배포 — 다음 배포 대기):** +- **Nifty 디자인 시스템 정렬**(카드 `.kx-card` Nifty 구조·`--fs-micro`/`--fw-*` 토큰·공용 제목클래스·테이블 `.kx-table` 6화면 수렴, font 179+weight 415 토큰화) +- **카드 이미지 3:4 프레임 + object-fit cover**(왜곡/잘림 제거) +- **메시지 관리 백엔드**(V48 sys_message + 렌더 API + 36샘플) +- **통계밴드 흰글자 2곳** · **푸터 "2026 © KINTEX..."** 4개국어 + +**병렬 진행 중:** 모바일 다국어+역할랜딩 · 잔여 데이터 보강(V49) · 산출물 문서 + +**설계·표준 문서 산출:** PLANNING v3.4(AI 토큰최소화 §8A) · design.md v2.4(공개 비주얼·모션) · docs/architecture/data.md(FK최소화·공통코드·view/mview·배치) · sso-hr-integration.md(Open SSO+HR) · DESIGN_SYSTEM_NIFTY.md · mobile-security-*.md · mobile-benchmark-*.md + +## 산출물(문서) 상태 + +| 산출물 | 타이밍 | 상태 | +|--------|--------|------| +| 개발계획서 | 개발 전/중 | 📋 생성 착수(PLANNING v3.4 기반) | +| 설계서(아키텍처·ERD·화면·API) | 개발 중 | 📋 생성 착수(design.md v2.4·architecture·backend 계약 기반) | +| 사용자 지침서 | **완성+QA 후**(최종 메뉴 반영) | ⏳ UI 정렬 안정화 후 — 지금 생성 시 재작업(정책: 최종 1회) | +| 운영자 지침서 | **완성+QA 후** | ⏳ 동일 | + +> 정책([[deliverables-update-policy]]): deliverables는 최초 1회 + 최종 1회. 개발계획서·설계서는 지금 생성 가능(내용 안정), 지침서는 현재 UI 정렬 파동이 끝난 뒤 최종 메뉴 기준으로 1회 생성. diff --git a/src/backend/src/main/java/com/zioinfo/kintex/system/message/MessageController.java b/src/backend/src/main/java/com/zioinfo/kintex/system/message/MessageController.java new file mode 100644 index 0000000..4f1782a --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/system/message/MessageController.java @@ -0,0 +1,103 @@ +package com.zioinfo.kintex.system.message; + +import com.zioinfo.kintex.auth.EventAccessGuard; +import com.zioinfo.kintex.auth.KintexPrincipal; +import com.zioinfo.kintex.common.ApiResponse; +import com.zioinfo.kintex.common.audit.Audited; +import com.zioinfo.kintex.system.SystemAccessGuard; +import jakarta.validation.Valid; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +/** + * 메시지 관리 API. 관리(CRUD)는 ADMIN(시스템관리 게이트), 렌더는 인증 사용자 공개. + * - GET /api/admin/messages 목록(category/locale 필터) [ADMIN] + * - GET /api/admin/messages/{code} 코드별 로케일 목록 [ADMIN] + * - POST /api/admin/messages 생성/수정(upsert) [ADMIN] + * - PUT /api/admin/messages/{code} 수정(upsert) [ADMIN] + * - DELETE /api/admin/messages/{code} 삭제(locale 옵션) [ADMIN] + * - POST /api/messages/{code}/render {params:{...}} → 치환 문자열 [인증] + */ +@RestController +public class MessageController { + + private final MessageService service; + private final EventAccessGuard authGuard; + private final SystemAccessGuard adminGuard; + + public MessageController(MessageService service, EventAccessGuard authGuard, SystemAccessGuard adminGuard) { + this.service = service; + this.authGuard = authGuard; + this.adminGuard = adminGuard; + } + + @GetMapping("/api/admin/messages") + public ApiResponse> list(@AuthenticationPrincipal KintexPrincipal principal, + @RequestParam(required = false) String category, + @RequestParam(required = false) String locale) { + adminGuard.requireAdmin(principal); + return ApiResponse.ok(service.list(principal.tenantId(), category, locale)); + } + + @GetMapping("/api/admin/messages/{code}") + public ApiResponse> byCode(@AuthenticationPrincipal KintexPrincipal principal, + @PathVariable String code) { + adminGuard.requireAdmin(principal); + return ApiResponse.ok(service.byCode(principal.tenantId(), code)); + } + + @Audited(action = "SYS_MESSAGE_SAVE", targetType = "sys_message") + @PostMapping("/api/admin/messages") + public ApiResponse create(@AuthenticationPrincipal KintexPrincipal principal, + @Valid @RequestBody MessageDto dto) { + adminGuard.requireAdmin(principal); + service.save(principal.tenantId(), dto); + return ApiResponse.ok(null); + } + + @Audited(action = "SYS_MESSAGE_SAVE", targetType = "sys_message") + @PutMapping("/api/admin/messages/{code}") + public ApiResponse update(@AuthenticationPrincipal KintexPrincipal principal, + @PathVariable String code, @Valid @RequestBody MessageDto dto) { + adminGuard.requireAdmin(principal); + MessageDto merged = new MessageDto(code, dto.locale(), dto.category(), dto.title(), + dto.template(), dto.useYn(), null); + service.save(principal.tenantId(), merged); + return ApiResponse.ok(null); + } + + @Audited(action = "SYS_MESSAGE_DELETE", targetType = "sys_message") + @DeleteMapping("/api/admin/messages/{code}") + public ApiResponse delete(@AuthenticationPrincipal KintexPrincipal principal, + @PathVariable String code, + @RequestParam(required = false) String locale) { + adminGuard.requireAdmin(principal); + service.delete(principal.tenantId(), code, locale); + return ApiResponse.ok(null); + } + + /** 렌더 — {params:{name:..,eventName:..}} 를 받아 template 치환 결과 반환. */ + @PostMapping("/api/messages/{code}/render") + public ApiResponse render(@AuthenticationPrincipal KintexPrincipal principal, + @PathVariable String code, + @RequestParam(required = false) String locale, + @RequestBody(required = false) RenderRequest body) { + authGuard.require(principal); + Map params = body == null || body.params() == null ? Map.of() : body.params(); + String loc = (locale != null && !locale.isBlank()) ? locale + : (body != null ? body.locale() : null); + String text = service.render(principal.tenantId(), code, loc, params); + return ApiResponse.ok(new RenderResult(code, loc, text)); + } + + /** 렌더 요청 본문 — params 는 {key}→값 매핑. locale 은 쿼리 우선, 없으면 본문. */ + public record RenderRequest(String locale, Map params) { + } + + /** 렌더 결과. */ + public record RenderResult(String code, String locale, String text) { + } +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/system/message/MessageDto.java b/src/backend/src/main/java/com/zioinfo/kintex/system/message/MessageDto.java new file mode 100644 index 0000000..e35f500 --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/system/message/MessageDto.java @@ -0,0 +1,17 @@ +package com.zioinfo.kintex.system.message; + +import jakarta.validation.constraints.NotBlank; + +/** + * 메시지 템플릿(sys_message 이식) — 알림/이메일/통지 문구를 코드·로케일별로 관리. + * template 은 {name}·{eventName} 등 중괄호 플레이스홀더를 포함하며, 렌더 시 파라미터로 치환된다. + */ +public record MessageDto( + @NotBlank String msgCode, + String locale, + @NotBlank String category, + @NotBlank String title, + @NotBlank String template, + String useYn, + String updatedAt) { +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/system/message/MessageMapper.java b/src/backend/src/main/java/com/zioinfo/kintex/system/message/MessageMapper.java new file mode 100644 index 0000000..d670534 --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/system/message/MessageMapper.java @@ -0,0 +1,51 @@ +package com.zioinfo.kintex.system.message; + +import org.apache.ibatis.annotations.*; + +import java.util.List; +import java.util.Map; + +/** + * 메시지 템플릿 매퍼(sys_message). 필터는 null-safe(값이 없으면 조건 무시) — <script> 미사용. + */ +@Mapper +public interface MessageMapper { + + @Select("SELECT msg_code AS \"msgCode\", locale, category, title, template, use_yn AS \"useYn\", " + + "to_char(updated_at,'YYYY-MM-DD\"T\"HH24:MI:SS') AS \"updatedAt\" FROM sys_message " + + "WHERE tenant_id = #{tenantId} " + + "AND (#{category} IS NULL OR category = #{category}) " + + "AND (#{locale} IS NULL OR locale = #{locale}) " + + "ORDER BY category, msg_code, locale") + List> findAll(@Param("tenantId") String tenantId, + @Param("category") String category, + @Param("locale") String locale); + + /** 코드로 로케일 목록 조회(관리 상세). */ + @Select("SELECT msg_code AS \"msgCode\", locale, category, title, template, use_yn AS \"useYn\", " + + "to_char(updated_at,'YYYY-MM-DD\"T\"HH24:MI:SS') AS \"updatedAt\" FROM sys_message " + + "WHERE tenant_id = #{tenantId} AND msg_code = #{msgCode} ORDER BY locale") + List> findByCode(@Param("tenantId") String tenantId, + @Param("msgCode") String msgCode); + + /** 렌더용 단건 조회 — 정확 로케일 우선, 없으면 ko 폴백은 서비스에서 처리. */ + @Select("SELECT msg_code AS \"msgCode\", locale, category, title, template, use_yn AS \"useYn\" " + + "FROM sys_message WHERE tenant_id = #{tenantId} AND msg_code = #{msgCode} " + + "AND locale = #{locale} AND use_yn = 'Y'") + Map findOne(@Param("tenantId") String tenantId, + @Param("msgCode") String msgCode, + @Param("locale") String locale); + + @Insert("INSERT INTO sys_message (tenant_id, msg_code, locale, category, title, template, use_yn) " + + "VALUES (#{tenantId}, #{msgCode}, COALESCE(#{locale},'ko'), #{category}, #{title}, #{template}, " + + "COALESCE(#{useYn},'Y')) " + + "ON CONFLICT (tenant_id, msg_code, locale) DO UPDATE SET category=EXCLUDED.category, " + + "title=EXCLUDED.title, template=EXCLUDED.template, use_yn=EXCLUDED.use_yn, updated_at=now()") + int upsert(Map p); + + @Delete("DELETE FROM sys_message WHERE tenant_id = #{tenantId} AND msg_code = #{msgCode} " + + "AND (#{locale} IS NULL OR locale = #{locale})") + int delete(@Param("tenantId") String tenantId, + @Param("msgCode") String msgCode, + @Param("locale") String locale); +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/system/message/MessageService.java b/src/backend/src/main/java/com/zioinfo/kintex/system/message/MessageService.java new file mode 100644 index 0000000..d833292 --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/system/message/MessageService.java @@ -0,0 +1,112 @@ +package com.zioinfo.kintex.system.message; + +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * 메시지 템플릿 조회/관리 + 렌더링. + *

렌더: template 의 {key} 를 params[key] 로 치환한다. 파라미터가 없으면 원본 {key} 를 유지하고(안전), + * 값이 null 이면 빈 문자열로 치환한다. 알림/이메일(W10)·통지에서 {@link #render}/{@link #renderTemplate} 재사용. + */ +@Service +public class MessageService { + + /** {key} — 영문/숫자/밑줄/점만 허용(중괄호 오탐 방지). */ + private static final Pattern PLACEHOLDER = Pattern.compile("\\{([a-zA-Z0-9_.]+)}"); + private static final String DEFAULT_TENANT = "KINTEX"; + private static final String DEFAULT_LOCALE = "ko"; + + private final MessageMapper mapper; + + public MessageService(MessageMapper mapper) { + this.mapper = mapper; + } + + public List list(String tenantId, String category, String locale) { + return mapper.findAll(tenant(tenantId), blankToNull(category), blankToNull(locale)) + .stream().map(MessageService::toDto).toList(); + } + + public List byCode(String tenantId, String msgCode) { + return mapper.findByCode(tenant(tenantId), msgCode).stream().map(MessageService::toDto).toList(); + } + + public void save(String tenantId, MessageDto dto) { + Map p = new HashMap<>(); + p.put("tenantId", tenant(tenantId)); + p.put("msgCode", dto.msgCode()); + p.put("locale", blankToNull(dto.locale())); + p.put("category", dto.category()); + p.put("title", dto.title()); + p.put("template", dto.template()); + p.put("useYn", dto.useYn()); + mapper.upsert(p); + } + + /** locale 이 null 이면 해당 코드의 전 로케일 삭제. */ + public void delete(String tenantId, String msgCode, String locale) { + mapper.delete(tenant(tenantId), msgCode, blankToNull(locale)); + } + + /** + * 코드+로케일 템플릿을 조회해 파라미터로 치환한 최종 문자열을 반환. + * 요청 로케일이 없으면 ko 로 폴백, 그래도 없으면 빈 문자열. + */ + public String render(String tenantId, String msgCode, String locale, Map params) { + String loc = (locale == null || locale.isBlank()) ? DEFAULT_LOCALE : locale; + Map row = mapper.findOne(tenant(tenantId), msgCode, loc); + if (row == null && !DEFAULT_LOCALE.equals(loc)) { + row = mapper.findOne(tenant(tenantId), msgCode, DEFAULT_LOCALE); + } + if (row == null) { + return ""; + } + return renderTemplate(String.valueOf(row.get("template")), params); + } + + /** template 의 {key} 를 params 로 치환(파라미터 없으면 원본 유지, null 이면 빈 문자열). */ + public String renderTemplate(String template, Map params) { + if (template == null || template.isEmpty()) { + return template == null ? "" : template; + } + Map safe = params == null ? Map.of() : params; + Matcher m = PLACEHOLDER.matcher(template); + StringBuilder sb = new StringBuilder(); + while (m.find()) { + String key = m.group(1); + String replacement; + if (safe.containsKey(key)) { + Object v = safe.get(key); + replacement = v == null ? "" : String.valueOf(v); + } else { + replacement = m.group(0); // 원본 {key} 유지 + } + m.appendReplacement(sb, Matcher.quoteReplacement(replacement)); + } + m.appendTail(sb); + return sb.toString(); + } + + private static String tenant(String tenantId) { + return (tenantId == null || tenantId.isBlank()) ? DEFAULT_TENANT : tenantId; + } + + private static String blankToNull(String s) { + return (s == null || s.isBlank()) ? null : s; + } + + private static MessageDto toDto(Map r) { + return new MessageDto( + str(r.get("msgCode")), str(r.get("locale")), str(r.get("category")), + str(r.get("title")), str(r.get("template")), str(r.get("useYn")), str(r.get("updatedAt"))); + } + + private static String str(Object o) { + return o == null ? null : String.valueOf(o); + } +} diff --git a/src/backend/src/main/resources/db/migration/V48__sys_message_templates.sql b/src/backend/src/main/resources/db/migration/V48__sys_message_templates.sql new file mode 100644 index 0000000..5c4b9f4 --- /dev/null +++ b/src/backend/src/main/resources/db/migration/V48__sys_message_templates.sql @@ -0,0 +1,115 @@ +-- V48 시스템관리 '메시지 관리' — 메시지 템플릿 리소스 + 샘플 전량 시드 (#35) +-- 목표: 알림/이메일/통지 문구를 코드·로케일별 템플릿({name}·{eventName} 등 중괄호 플레이스홀더)으로 중앙 관리. +-- 렌더 API가 template 의 {key} 를 파라미터로 치환해 최종 문자열을 만든다(코드 계층). +-- 불변: V1~V47 파일 불변. 본 마이그레이션은 순증(additive)·멱등(ON CONFLICT)만 수행한다. +-- 테넌트 표준(V31): tenant_id DEFAULT 'KINTEX', 복합 PK 선두 (tenant_id, msg_code, locale). +-- 보안 불변(§0-3): 템플릿은 문구만 저장 — 비밀번호·시크릿·PII 실값 미저장(치환 파라미터는 런타임 전달). + +-- ============================================================================ +-- 1) 테이블 — 메시지 템플릿 +-- ============================================================================ +CREATE TABLE IF NOT EXISTS sys_message ( + tenant_id varchar(40) NOT NULL DEFAULT 'KINTEX', + msg_code varchar(60) NOT NULL, -- 메시지 코드(예: AUCTION_INVITE) + locale varchar(10) NOT NULL DEFAULT 'ko', -- ko/en/zh/ja + category varchar(40) NOT NULL, -- 분류(AUCTION/SETTLEMENT/ACCOUNT/VISITOR/MARKETING) + title varchar(300) NOT NULL, -- 제목/요약(플레이스홀더 포함 가능) + template text NOT NULL, -- 본문 템플릿({key} 플레이스홀더 포함) + use_yn char(1) NOT NULL DEFAULT 'Y', + updated_at timestamptz NOT NULL DEFAULT now(), + PRIMARY KEY (tenant_id, msg_code, locale) +); +CREATE INDEX IF NOT EXISTS idx_sys_message_cat ON sys_message (tenant_id, category, msg_code); + +-- ============================================================================ +-- 2) 샘플 전량 시드 — 주요 메시지 리소스 전 카테고리 × (ko/en/zh/ja) +-- ON CONFLICT DO UPDATE 로 멱등. 플레이스홀더는 중괄호 {key}. +-- ============================================================================ +INSERT INTO sys_message (tenant_id, msg_code, locale, category, title, template) VALUES +-- AUCTION: 옥션(입찰) 통지 +('KINTEX','AUCTION_INVITE','ko','AUCTION','[{eventName}] 장치공사 입찰 안내', + '{companyName} 담당자님, {eventName} 부스({boothNo}) 장치공사 입찰에 초대합니다. 마감일: {dueDate}. 킨텍스 AI 전시시스템에서 견적을 제출해 주세요.'), +('KINTEX','AUCTION_INVITE','en','AUCTION','[{eventName}] Booth Construction Bid Invitation', + 'Dear {companyName}, you are invited to bid for booth ({boothNo}) construction of {eventName}. Deadline: {dueDate}. Please submit your quotation on the KINTEX AI system.'), +('KINTEX','AUCTION_INVITE','zh','AUCTION','[{eventName}] 展位施工招标邀请', + '{companyName} 负责人您好,诚邀您参与 {eventName} 展位({boothNo})施工招标。截止日期:{dueDate}。请在 KINTEX AI 系统提交报价。'), +('KINTEX','AUCTION_INVITE','ja','AUCTION','[{eventName}] ブース施工入札のご案内', + '{companyName} ご担当者様、{eventName} ブース({boothNo})施工の入札にご招待します。締切: {dueDate}。KINTEX AI システムで見積をご提出ください。'), +-- AUCTION: 낙찰 +('KINTEX','AUCTION_AWARD','ko','AUCTION','[{eventName}] 낙찰 안내', + '{companyName} 담당자님, {eventName} 장치공사 입찰에 낙찰되셨습니다. 낙찰 금액: {amount}원. 세부 일정은 시스템에서 확인해 주세요.'), +('KINTEX','AUCTION_AWARD','en','AUCTION','[{eventName}] Award Notice', + 'Dear {companyName}, you have won the construction bid for {eventName}. Award amount: {amount} KRW. Please check the schedule on the system.'), +('KINTEX','AUCTION_AWARD','zh','AUCTION','[{eventName}] 中标通知', + '{companyName} 负责人您好,恭喜您中标 {eventName} 施工项目。中标金额:{amount} 韩元。请在系统查看详细日程。'), +('KINTEX','AUCTION_AWARD','ja','AUCTION','[{eventName}] 落札のご案内', + '{companyName} ご担当者様、{eventName} 施工入札に落札されました。落札金額: {amount}ウォン。詳細日程はシステムでご確認ください。'), +-- SETTLEMENT: 정산 청구 +('KINTEX','SETTLE_INVOICE','ko','SETTLEMENT','[정산] 청구서 발행 안내', + '{companyName} 담당자님, 청구서({invoiceNo})가 발행되었습니다. 청구 금액: {amount}원, 납부 기한: {dueDate}. 감사합니다.'), +('KINTEX','SETTLE_INVOICE','en','SETTLEMENT','[Settlement] Invoice Issued', + 'Dear {companyName}, invoice ({invoiceNo}) has been issued. Amount: {amount} KRW, due by {dueDate}. Thank you.'), +('KINTEX','SETTLE_INVOICE','zh','SETTLEMENT','[结算] 账单已开具', + '{companyName} 负责人您好,账单({invoiceNo})已开具。金额:{amount} 韩元,付款期限:{dueDate}。谢谢。'), +('KINTEX','SETTLE_INVOICE','ja','SETTLEMENT','[精算] 請求書発行のご案内', + '{companyName} ご担当者様、請求書({invoiceNo})が発行されました。金額: {amount}ウォン、支払期限: {dueDate}。ありがとうございます。'), +-- SETTLEMENT: 마감임박 +('KINTEX','SETTLE_DUE','ko','SETTLEMENT','[정산] 납부 마감 임박 안내', + '{companyName} 담당자님, 청구서({invoiceNo})의 납부 기한이 {dueDate}로 임박했습니다. 기한 내 납부 부탁드립니다.'), +('KINTEX','SETTLE_DUE','en','SETTLEMENT','[Settlement] Payment Due Soon', + 'Dear {companyName}, payment for invoice ({invoiceNo}) is due on {dueDate}. Please complete payment on time.'), +('KINTEX','SETTLE_DUE','zh','SETTLEMENT','[结算] 付款截止临近', + '{companyName} 负责人您好,账单({invoiceNo})付款期限 {dueDate} 临近,请按时付款。'), +('KINTEX','SETTLE_DUE','ja','SETTLEMENT','[精算] 支払期限が近づいています', + '{companyName} ご担当者様、請求書({invoiceNo})の支払期限 {dueDate} が近づいています。期限内のお支払をお願いします。'), +-- ACCOUNT: 환영 +('KINTEX','WELCOME','ko','ACCOUNT','환영합니다, {name}님', + '{name}님, KINTEX AI 전시·행사시스템에 오신 것을 환영합니다. {eventName} 준비를 함께 시작해 보세요.'), +('KINTEX','WELCOME','en','ACCOUNT','Welcome, {name}', + 'Hi {name}, welcome to the KINTEX AI Exhibition System. Let us start preparing {eventName} together.'), +('KINTEX','WELCOME','zh','ACCOUNT','欢迎您,{name}', + '{name} 您好,欢迎使用 KINTEX AI 展览系统。让我们一起开始筹备 {eventName} 吧。'), +('KINTEX','WELCOME','ja','ACCOUNT','ようこそ、{name}様', + '{name}様、KINTEX AI 展示会システムへようこそ。{eventName} の準備を一緒に始めましょう。'), +-- ACCOUNT: 비밀번호 재설정 +('KINTEX','PASSWORD_RESET','ko','ACCOUNT','[보안] 비밀번호 재설정 안내', + '{name}님, 비밀번호 재설정을 요청하셨습니다. 아래 링크는 {expireMin}분간 유효합니다: {resetLink}. 본인이 아니라면 무시하세요.'), +('KINTEX','PASSWORD_RESET','en','ACCOUNT','[Security] Reset Your Password', + 'Hi {name}, you requested a password reset. The link is valid for {expireMin} minutes: {resetLink}. Ignore if this was not you.'), +('KINTEX','PASSWORD_RESET','zh','ACCOUNT','[安全] 重置密码', + '{name} 您好,您申请了重置密码。链接 {expireMin} 分钟内有效:{resetLink}。若非本人操作请忽略。'), +('KINTEX','PASSWORD_RESET','ja','ACCOUNT','[セキュリティ] パスワード再設定のご案内', + '{name}様、パスワード再設定のリクエストを受け付けました。リンクは {expireMin} 分間有効です: {resetLink}。心当たりがない場合は無視してください。'), +-- VISITOR: 체크인 +('KINTEX','CHECKIN','ko','VISITOR','[{eventName}] 체크인 완료', + '{name}님, {eventName} 체크인이 완료되었습니다. 관심 부스({boothNo})를 둘러보세요. 즐거운 관람 되세요.'), +('KINTEX','CHECKIN','en','VISITOR','[{eventName}] Check-in Complete', + 'Hi {name}, your check-in for {eventName} is complete. Explore booth ({boothNo}). Enjoy the show.'), +('KINTEX','CHECKIN','zh','VISITOR','[{eventName}] 签到完成', + '{name} 您好,您在 {eventName} 的签到已完成。请参观展位({boothNo})。祝您观展愉快。'), +('KINTEX','CHECKIN','ja','VISITOR','[{eventName}] チェックイン完了', + '{name}様、{eventName} のチェックインが完了しました。ブース({boothNo})をご覧ください。ごゆっくりお楽しみください。'), +-- VISITOR: 배지 발급 +('KINTEX','BADGE_ISSUE','ko','VISITOR','[{eventName}] 배지 발급 안내', + '{name}님, 관람 배지({badgeNo})가 발급되었습니다. 입장 시 QR 코드를 제시해 주세요.'), +('KINTEX','BADGE_ISSUE','en','VISITOR','[{eventName}] Badge Issued', + 'Hi {name}, your badge ({badgeNo}) for {eventName} has been issued. Present the QR code at entry.'), +('KINTEX','BADGE_ISSUE','zh','VISITOR','[{eventName}] 参观证已发放', + '{name} 您好,您在 {eventName} 的参观证({badgeNo})已发放。入场时请出示二维码。'), +('KINTEX','BADGE_ISSUE','ja','VISITOR','[{eventName}] バッジ発行のご案内', + '{name}様、{eventName} の入場バッジ({badgeNo})が発行されました。入場時に QR コードをご提示ください。'), +-- MARKETING: EDM(홍보 메일) +('KINTEX','EDM','ko','MARKETING','[{eventName}] 사전등록 안내', + '{name}님, {eventName}이(가) {eventDate}에 열립니다. 지금 사전등록하고 다양한 혜택을 받으세요.'), +('KINTEX','EDM','en','MARKETING','[{eventName}] Pre-registration Open', + 'Hi {name}, {eventName} opens on {eventDate}. Pre-register now to enjoy exclusive benefits.'), +('KINTEX','EDM','zh','MARKETING','[{eventName}] 预登记开放', + '{name} 您好,{eventName} 将于 {eventDate} 举行。立即预登记,享受专属优惠。'), +('KINTEX','EDM','ja','MARKETING','[{eventName}] 事前登録のご案内', + '{name}様、{eventName} が {eventDate} に開催されます。今すぐ事前登録して特典をお受け取りください。') +ON CONFLICT (tenant_id, msg_code, locale) DO UPDATE + SET category = EXCLUDED.category, + title = EXCLUDED.title, + template = EXCLUDED.template, + use_yn = 'Y', + updated_at = now(); diff --git a/src/backend/src/main/resources/db/migration/V49__demo_seed_empty_tables.sql b/src/backend/src/main/resources/db/migration/V49__demo_seed_empty_tables.sql new file mode 100644 index 0000000..bd7445c --- /dev/null +++ b/src/backend/src/main/resources/db/migration/V49__demo_seed_empty_tables.sql @@ -0,0 +1,220 @@ +-- ============================================================================ +-- V49 — 잔여 빈 테이블 전수 보강 (화면이 쓰나 비어 있던 12개 도메인 테이블) +-- ============================================================================ +-- 배경: V42 대량시드(40테이블)·V47(e-2026-live 폐루프) 이후에도, CREATE 는 되었으나 +-- INSERT 가 전혀 없던(=화면에서 "빈 상태") 테이블이 다수 남아 있었다(매퍼 감사). +-- 대상(전부 매퍼/서비스가 조회): +-- render_job · cms_media · logistics_equipment_request · logistics_rental_order · +-- logistics_inbound · mail_log · edm_followup_log(+draft campaign) · +-- webhook_subscription · webhook_delivery · login_history · sys_role_menu · audit_log +-- 스코프: 이벤트 종속 도메인은 두 데모 행사(e-2026-live=오늘 · e-2026-smf=8월) 모두 채워 +-- 행사 전환 시에도 물류/렌탈/입고/렌더/팔로업이 보이게 한다(고아 레코드 금지 — 기존 booth/lead/company/menu 참조). +-- 원칙: 멱등(ON CONFLICT DO NOTHING) · tenant_id='KINTEX' · PII 더미(홍길동·010-0000-*·*@kintex.test) · +-- 기존 시드 불변. 서버 최신 V47, 진행 중 V48(메시지관리)과 충돌 회피 → V49 확정. +-- +-- 의도적으로 비워 두는 테이블(보안·운영성): +-- error_log(운영 500 요약만 기록) · password_reset(1회용 재설정 토큰) · +-- webhook_inbound_endpoint / webhook_inbound_event(secret_enc AES-256-GCM 봉인은 +-- 앱 부팅 시더 전용 — SQL 평문 저장 금지, V35 주석 준수). +-- ============================================================================ + + +-- ============================================================================ +-- 1) render_job — 부스별 나노바나나 렌더 결과(상태 분포: DONE·RUNNING·QUEUED·FAILED) +-- ============================================================================ +-- e-2026-live 부스(booth-live-01..06) + e-2026-smf 부스(booth-smf-a102/b101..b106). +INSERT INTO render_job (id, event_id, booth_id, shot_preset, status, image_url, schema_hash, model_version, error_message, created_at, updated_at) VALUES + -- e-2026-live + ('rj-live-01','e-2026-live','booth-live-01','S1','DONE','/uploads/render/rj-live-01.png','sha1-live01a','gemini-3.1-flash-image-preview',NULL, now()-interval '5 hour', now()-interval '5 hour'), + ('rj-live-02','e-2026-live','booth-live-01','S3','DONE','/uploads/render/rj-live-02.png','sha1-live01b','gemini-3.1-flash-image-preview',NULL, now()-interval '5 hour', now()-interval '4 hour'), + ('rj-live-03','e-2026-live','booth-live-02','S1','DONE','/uploads/render/rj-live-03.png','sha1-live02a','gemini-3.1-flash-image-preview',NULL, now()-interval '4 hour', now()-interval '3 hour'), + ('rj-live-04','e-2026-live','booth-live-03','S2','RUNNING',NULL,'sha1-live03a','gemini-3.1-flash-image-preview',NULL, now()-interval '20 minute', now()-interval '2 minute'), + ('rj-live-05','e-2026-live','booth-live-04','S1','QUEUED',NULL,'sha1-live04a',NULL,NULL, now()-interval '8 minute', now()-interval '8 minute'), + ('rj-live-06','e-2026-live','booth-live-05','S4','FAILED',NULL,'sha1-live05a','gemini-3.1-flash-image-preview','생성 시간 초과 — 재시도 대기(요약).', now()-interval '2 hour', now()-interval '2 hour'), + ('rj-live-07','e-2026-live','booth-live-06','S1','DONE','/uploads/render/rj-live-07.png','sha1-live06a','gemini-3.1-flash-image-preview',NULL, now()-interval '6 hour', now()-interval '6 hour'), + -- e-2026-smf + ('rj-smf-01','e-2026-smf','booth-smf-a102','S1','DONE','/uploads/render/rj-smf-01.png','sha1-smfa102','gemini-3.1-flash-image-preview',NULL, TIMESTAMPTZ '2026-07-20 10:00+09', TIMESTAMPTZ '2026-07-20 10:03+09'), + ('rj-smf-02','e-2026-smf','booth-smf-b101','S2','DONE','/uploads/render/rj-smf-02.png','sha1-smfb101','gemini-3.1-flash-image-preview',NULL, TIMESTAMPTZ '2026-07-21 11:00+09', TIMESTAMPTZ '2026-07-21 11:04+09'), + ('rj-smf-03','e-2026-smf','booth-smf-b102','S1','DONE','/uploads/render/rj-smf-03.png','sha1-smfb102','gemini-3.1-flash-image-preview',NULL, TIMESTAMPTZ '2026-07-22 09:30+09', TIMESTAMPTZ '2026-07-22 09:33+09'), + ('rj-smf-04','e-2026-smf','booth-smf-b103','S3','QUEUED',NULL,'sha1-smfb103',NULL,NULL, TIMESTAMPTZ '2026-07-23 14:00+09', TIMESTAMPTZ '2026-07-23 14:00+09'), + ('rj-smf-05','e-2026-smf','booth-smf-b104','S5','FAILED',NULL,'sha1-smfb104','gemini-3.1-flash-image-preview','입력 평면도 해상도 부족 — 재업로드 요망(요약).', TIMESTAMPTZ '2026-07-23 15:20+09', TIMESTAMPTZ '2026-07-23 15:21+09') +ON CONFLICT (id) DO NOTHING; + + +-- ============================================================================ +-- 2) cms_media — 미디어 라이브러리(전역 + 행사별) +-- ============================================================================ +INSERT INTO cms_media (id, event_id, file_name, url, content_type, size_bytes, alt_text, uploader_name, created_at) VALUES + ('med-global-01', NULL, 'kintex-logo.svg', '/uploads/cms/kintex-logo.svg', 'image/svg+xml', 18240, 'KINTEX AI 전시·행사시스템 로고', '관리자', now()-interval '30 day'), + ('med-global-02', NULL, 'hero-hall.jpg', '/uploads/cms/hero-hall.jpg', 'image/jpeg', 482113, '킨텍스 전시홀 메인 히어로', '관리자', now()-interval '25 day'), + ('med-global-03', NULL, 'brochure-cover.png', '/uploads/cms/brochure-cover.png', 'image/png', 231544, '공식 도록 표지', '관리자', now()-interval '20 day'), + ('med-live-01','e-2026-live', 'smart-auto-poster.jpg', '/uploads/cms/smart-auto-poster.jpg','image/jpeg', 356820, '스마트 물류·자동화 코리아 포스터', '홍길동', now()-interval '10 day'), + ('med-live-02','e-2026-live', 'floorplan-live.png', '/uploads/cms/floorplan-live.png', 'image/png', 512900, 'e-2026-live 홀 배치도', '홍길동', now()-interval '7 day'), + ('med-live-03','e-2026-live', 'sponsor-banner.jpg', '/uploads/cms/sponsor-banner.jpg', 'image/jpeg', 198340, '스폰서 배너(다이아몬드)', '홍길동', now()-interval '3 day'), + ('med-smf-01','e-2026-smf', 'smf-keyvisual.jpg', '/uploads/cms/smf-keyvisual.jpg', 'image/jpeg', 421077, '스마트 팩토리 엑스포 키비주얼', '관리자', TIMESTAMPTZ '2026-07-15 10:00+09'), + ('med-smf-02','e-2026-smf', 'smf-map.png', '/uploads/cms/smf-map.png', 'image/png', 488213, 'e-2026-smf 전시장 안내도', '관리자', TIMESTAMPTZ '2026-07-16 10:00+09'), + ('med-smf-03','e-2026-smf', 'smf-speaker.jpg', '/uploads/cms/smf-speaker.jpg', 'image/jpeg', 167902, '기조연설 연사 프로필(더미)', '관리자', TIMESTAMPTZ '2026-07-17 10:00+09') +ON CONFLICT (id) DO NOTHING; + + +-- ============================================================================ +-- 3) logistics_equipment_request — 부대장비 신청(마스터 스냅샷, 상태 분포) +-- ============================================================================ +INSERT INTO logistics_equipment_request + (id, event_id, equipment_id, equipment_name, unit_price, booth_no, quantity, use_date, days, amount, status, note, reject_reason, requested_by, decided_by, decided_at) VALUES + ('ler-live-01','e-2026-live','leq-fork-25','지게차 2.5t',180000,'L-101',1, CURRENT_DATE, 1, 180000,'approved','반입일 오전 상차','', 'demo-exhibitor','demo-organizer', now()-interval '1 day'), + ('ler-live-02','e-2026-live','leq-manp-01','상하차 인력', 90000,'L-102',2, CURRENT_DATE, 1, 180000,'requested','대형 장비 상하차 인력 2인','', 'demo-exhibitor',NULL,NULL), + ('ler-live-03','e-2026-live','leq-lift-01','고소작업 리프트',150000,'L-103',1, CURRENT_DATE+1, 1, 150000,'done','트러스 설치 완료','', 'demo-exhibitor','demo-organizer', now()-interval '2 day'), + ('ler-smf-01','e-2026-smf','leq-fork-05','지게차 5t',260000,'A-102',1, DATE '2026-08-04', 1, 260000,'approved','반입일 대형 화물 상차','', 'demo-exhibitor','demo-organizer', TIMESTAMPTZ '2026-07-28 10:00+09'), + ('ler-smf-02','e-2026-smf','leq-lift-01','고소작업 리프트',150000,'B-101',1, DATE '2026-08-04', 2, 300000,'requested','2일간 사이니지 설치','', 'demo-exhibitor',NULL,NULL), + ('ler-smf-03','e-2026-smf','leq-crane-5','이동식 크레인 5t',420000,'B-104',1, DATE '2026-08-05', 1, 420000,'rejected','','중량물 크레인은 안전관리계획서 선제출 필요', 'demo-exhibitor','demo-organizer', TIMESTAMPTZ '2026-07-29 14:00+09') +ON CONFLICT (id) DO NOTHING; + + +-- ============================================================================ +-- 4) logistics_rental_order — 렌탈 품목 주문(마스터 스냅샷, 상태 분포) +-- ============================================================================ +INSERT INTO logistics_rental_order + (id, event_id, item_id, item_name, daily_price, booth_no, quantity, start_date, end_date, days, amount, status, note, ordered_by, decided_by, decided_at) VALUES + ('lro-live-01','e-2026-live','lri-tv-55','55인치 모니터',60000,'L-101',2, CURRENT_DATE, CURRENT_DATE+2, 3, 360000,'confirmed','부스 시연용 2대','demo-exhibitor','demo-organizer', now()-interval '1 day'), + ('lro-live-02','e-2026-live','lri-chair-01','상담용 의자',3000,'L-101',10, CURRENT_DATE, CURRENT_DATE+2, 3, 90000,'ordered','상담 부스 의자','demo-exhibitor',NULL,NULL), + ('lro-live-03','e-2026-live','lri-info-01','인포메이션 데스크',25000,'L-102',1, CURRENT_DATE, CURRENT_DATE+2, 3, 75000,'delivered','안내 데스크 설치 완료','demo-exhibitor','demo-organizer', now()-interval '6 hour'), + ('lro-smf-01','e-2026-smf','lri-table-01','접이식 테이블',8000,'A-102',5, DATE '2026-08-04', DATE '2026-08-07', 4, 160000,'confirmed','상담존 테이블','demo-exhibitor','demo-organizer', TIMESTAMPTZ '2026-07-28 11:00+09'), + ('lro-smf-02','e-2026-smf','lri-spot-01','LED 스포트 조명',5000,'B-101',12, DATE '2026-08-04', DATE '2026-08-07', 4, 240000,'ordered','제품 진열 조명','demo-exhibitor',NULL,NULL), + ('lro-smf-03','e-2026-smf','lri-shelf-01','진열 선반',12000,'B-103',3, DATE '2026-08-04', DATE '2026-08-07', 4, 144000,'cancelled','발주 취소(부스 설계 변경)','demo-exhibitor','demo-organizer', TIMESTAMPTZ '2026-07-30 09:00+09') +ON CONFLICT (id) DO NOTHING; + + +-- ============================================================================ +-- 5) logistics_inbound — 반입 화물 추적(운송장, 상태 분포) +-- ============================================================================ +INSERT INTO logistics_inbound + (id, event_id, tracking_no, carrier, booth_no, item_desc, quantity, status, eta_date, warehouse_at, delivered_at, note, registered_by, updated_by) VALUES + ('lib-live-01','e-2026-live','CJ-1000-2201','CJ대한통운','L-101','AMR 시연 장비 3종',3,'delivered_booth', CURRENT_DATE, now()-interval '8 hour', now()-interval '3 hour','부스 전달 완료','demo-exhibitor','demo-organizer'), + ('lib-live-02','e-2026-live','LO-2200-3310','로젠택배','L-102','비전 검사 모듈 팔레트',1,'arrived_warehouse', CURRENT_DATE, now()-interval '2 hour', NULL,'창고 대기 — 부스 전달 예정','demo-exhibitor','demo-organizer'), + ('lib-live-03','e-2026-live','HJ-3300-4420','한진택배','L-103','협동로봇 본체',2,'inbound_scheduled', CURRENT_DATE+1, NULL, NULL,'익일 입고 예정','demo-exhibitor',NULL), + ('lib-smf-01','e-2026-smf','CJ-5000-1102','CJ대한통운','A-102','스마트팩토리 데모 셀',1,'registered', DATE '2026-08-03', NULL, NULL,'반입 신청 등록','demo-exhibitor',NULL), + ('lib-smf-02','e-2026-smf','LO-5100-2203','로젠택배','B-101','전자 사이니지 패널 6매',6,'inbound_scheduled', DATE '2026-08-03', NULL, NULL,'개막 전일 입고','demo-exhibitor','demo-organizer') +ON CONFLICT (id) DO NOTHING; + + +-- ============================================================================ +-- 6) mail_log — 발송 이력(제목/마스킹 수신자만, 본문·원문 미저장) +-- ============================================================================ +INSERT INTO mail_log (tenant_id, id, category, ref_id, subject, recipient_masked, status, detail, created_at) VALUES + ('KINTEX','ml-0001','AUCTION_OPEN','AUC-LIVE-01','[킨텍스] L-101 아일랜드 부스 장치공사 옥션 초대','han***@kintex.test', 'sent','초대 공사업체 3사 발송', now()-interval '3 day'), + ('KINTEX','ml-0002','AUCTION_OPEN','AUC-LIVE-02','[킨텍스] 메인 통로 전기·조명 설치 옥션 초대','del***@kintex.test', 'sent','초대 공사업체 3사 발송', now()-interval '2 day'), + ('KINTEX','ml-0003','EDM','cp-live-001','[스마트 물류·자동화 코리아] 개막 안내','vis***@kintex.test', 'sent','사전등록자 5,400명 대상 발송', now()-interval '1 day'), + ('KINTEX','ml-0004','EDM','cp-live-002','[VIP 초대] 바이어 상담회 안내','buy***@kintex.test', 'sent','VIP 1,200명 대상 발송 예약', now()-interval '6 hour'), + ('KINTEX','ml-0005','PASSWORD_RESET',NULL,'[킨텍스] 비밀번호 재설정 안내','dem***@kintex.test','sent','재설정 링크 발송(유효 30분)', now()-interval '5 hour'), + ('KINTEX','ml-0006','EDM','cp-smf-001','[스마트 팩토리 엑스포] 사전등록 오픈 안내','pre***@kintex.test','sent','사전등록 안내 발송', TIMESTAMPTZ '2026-07-18 10:00+09'), + ('KINTEX','ml-0007','AUCTION_OPEN','AUC-2026-0511','[킨텍스] D-1 옥외 사이니지 설치 옥션 초대','sec***@kintex.test','failed','수신 도메인 일시 오류 — 재발송 대상', TIMESTAMPTZ '2026-07-25 14:00+09'), + ('KINTEX','ml-0008','EDM','cp-smf-003','[스마트 팩토리 엑스포] 세미나 프로그램 안내','edu***@kintex.test','skipped','수신거부 목록 필터링', TIMESTAMPTZ '2026-07-27 10:00+09') +ON CONFLICT (tenant_id, id) DO NOTHING; + + +-- ============================================================================ +-- 7) edm_followup_log(+ draft campaign) — 핫리드(score>=80) 팔로업 자동 초안 1회 +-- ============================================================================ +-- 리드당 초안 캠페인(draft) 1건 생성 후 followup_log 로 링크(멱등 키: UNIQUE(event_id, lead_id)). +INSERT INTO edm_campaign (id, event_id, name, status, audience, meta, open_rate, click_rate) VALUES + ('cp-fu-live-001','e-2026-live','[팔로업] 스마트 물류 AMR 커스텀 제안','draft', 1, 'AI 자동 초안 · ld-live-001', '-','-'), + ('cp-fu-live-002','e-2026-live','[팔로업] AI 비전 검사 모듈 파일럿 안내','draft', 1, 'AI 자동 초안 · ld-live-002', '-','-'), + ('cp-fu-smf-001','e-2026-smf','[팔로업] 협동로봇 HR-A1 도입 제안','draft', 1, 'AI 자동 초안 · ld-smf-006', '-','-'), + ('cp-fu-smf-002','e-2026-smf','[팔로업] 협동로봇 HR-A1 견적 안내','draft', 1, 'AI 자동 초안 · ld-smf-001', '-','-'), + ('cp-fu-smf-003','e-2026-smf','[팔로업] AI 비전 검사 모듈 사례집','draft', 1, 'AI 자동 초안 · ld-smf-003', '-','-') +ON CONFLICT (id) DO NOTHING; + +INSERT INTO edm_followup_log (tenant_id, id, event_id, lead_id, campaign_id, hot_score, body_draft, created_at) VALUES + ('KINTEX','fu-live-001','e-2026-live','ld-live-001','cp-fu-live-001',92, + '안녕하세요, 홍 팀장님. 스마트 물류 AMR 커스텀 제안서를 준비했습니다. 다음 주 미팅 가능하실까요?', now()-interval '3 hour'), + ('KINTEX','fu-live-002','e-2026-live','ld-live-002','cp-fu-live-002',85, + '안녕하세요, 김 님. 요청하신 AI 비전 검사 모듈 견적서와 파일럿 프로그램 안내드립니다.', now()-interval '5 hour'), + ('KINTEX','fu-smf-006','e-2026-smf','ld-smf-006','cp-fu-smf-001',91, + '안녕하세요, 문 팀장님. 협동로봇 HR-A1 도입 제안서를 보내드립니다.', TIMESTAMPTZ '2026-08-01 15:00+09'), + ('KINTEX','fu-smf-001','e-2026-smf','ld-smf-001','cp-fu-smf-002',87, + '안녕하세요, 김 담당자님. 협동로봇 HR-A1 견적서를 준비했습니다.', TIMESTAMPTZ '2026-08-01 15:10+09'), + ('KINTEX','fu-smf-003','e-2026-smf','ld-smf-003','cp-fu-smf-003',82, + '안녕하세요, 박 소장님. AI 비전 검사 모듈 도입 사례집을 보내드립니다.', TIMESTAMPTZ '2026-08-01 15:20+09') +-- 멱등 키는 업무 유니크 (event_id, lead_id) — V47/V42 가 동일 리드에 이미 초안을 시드했으면(다른 id) 건너뛴다. +-- (id PK 는 신규이므로 실충돌은 항상 (event_id, lead_id) 쪽에서 발생.) +ON CONFLICT (event_id, lead_id) DO NOTHING; + + +-- ============================================================================ +-- 8) webhook_subscription — 아웃바운드 구독(발송 disabled 스텁) +-- ============================================================================ +-- secret_hash = SHA-256(secret) hex 자리(원문 미저장·발송 disabled 이므로 검증 불요). 데모 고정 해시. +INSERT INTO webhook_subscription (tenant_id, id, event_type, callback_url, secret_hash, description, active, created_by) VALUES + ('KINTEX','whs-001','lead.hot', 'https://hooks.example.test/kintex/lead-hot', '3a7bd3e2360a3d29eea436fcfb7e44c735d117c42d1c1835420b6b9942dd4f1b','핫리드 발생 시 CRM 연동(스텁)', true, 'system-seed'), + ('KINTEX','whs-002','invoice.paid', 'https://hooks.example.test/kintex/invoice-paid','5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8','청구 완납 시 회계 연동(스텁)', true, 'system-seed'), + ('KINTEX','whs-003','auction.awarded','https://hooks.example.test/kintex/award', '6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b','옥션 낙찰 시 계약 연동(스텁)', true, 'system-seed'), + ('KINTEX','whs-004','visitor.checkin','https://hooks.example.test/kintex/checkin', 'd4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35','현장 체크인 실시간 연동(스텁)', false, 'system-seed') +ON CONFLICT (id) DO NOTHING; + + +-- ============================================================================ +-- 9) webhook_delivery — 아웃바운드 발송 큐(status=disabled 스텁, 실 HTTP 콜 없음) +-- ============================================================================ +INSERT INTO webhook_delivery (tenant_id, id, subscription_id, event_type, payload_json, status, note, created_at) VALUES + ('KINTEX','whd-001','whs-001','lead.hot', + '{"eventId":"e-2026-live","leadId":"ld-live-001","score":92}'::text,'disabled','스텁 — 실발송 없음', now()-interval '3 hour'), + ('KINTEX','whd-002','whs-002','invoice.paid', + '{"eventId":"e-2026-live","invoiceId":"inv-live-01","total":7187400}'::text,'disabled','스텁 — 실발송 없음', now()-interval '5 day'), + ('KINTEX','whd-003','whs-003','auction.awarded', + '{"eventId":"e-2026-live","auctionId":"AUC-LIVE-01","awardId":"aw-live-01"}'::text,'disabled','스텁 — 실발송 없음', now()-interval '1 day'), + ('KINTEX','whd-004','whs-001','lead.hot', + '{"eventId":"e-2026-smf","leadId":"ld-smf-006","score":91}'::text,'disabled','스텁 — 실발송 없음', TIMESTAMPTZ '2026-08-01 15:00+09') +ON CONFLICT (id) DO NOTHING; + + +-- ============================================================================ +-- 10) login_history — 로그인 이력(마스킹 이메일·IP, 결과 분포) +-- ============================================================================ +-- id 는 bigserial → 명시하지 않고 자동 채번. 재실행 시 중복 방지 위해 NOT EXISTS 가드. +INSERT INTO login_history (tenant_id, user_id, email_masked, result, fail_reason, ip_masked, user_agent, created_at) +SELECT * FROM (VALUES + ('KINTEX','demo-organizer', 'org***@kintex.test','SUCCESS',NULL, '203.0.113.*', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/126', now()-interval '2 hour'), + ('KINTEX','demo-exhibitor', 'exh***@kintex.test','SUCCESS',NULL, '203.0.113.*', 'Mozilla/5.0 (Macintosh; Intel Mac OS X) Safari/17', now()-interval '5 hour'), + ('KINTEX','demo-contractor','con***@kintex.test','FAIL', 'BAD_CREDENTIAL','198.51.100.*','Mozilla/5.0 (Windows NT 10.0; Win64; x64) Edge/126', now()-interval '1 day'), + ('KINTEX','demo-contractor','con***@kintex.test','SUCCESS',NULL, '198.51.100.*','Mozilla/5.0 (Windows NT 10.0; Win64; x64) Edge/126', now()-interval '23 hour'), + ('KINTEX','demo-visitor', 'vis***@kintex.test','SUCCESS',NULL, '203.0.113.*', 'Mozilla/5.0 (Linux; Android 14) Chrome/126 Mobile', now()-interval '40 minute'), + ('KINTEX', NULL, 'unk***@kintex.test','FAIL', 'BAD_CREDENTIAL','198.51.100.*','Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/125',now()-interval '3 day'), + ('KINTEX','demo-exhibitor', 'exh***@kintex.test','LOCKED', 'LOCKED', '198.51.100.*','Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/126',now()-interval '2 day') +) AS v(tenant_id, user_id, email_masked, result, fail_reason, ip_masked, user_agent, created_at) +WHERE NOT EXISTS (SELECT 1 FROM login_history WHERE user_agent LIKE 'Mozilla/5.0%' AND ip_masked IN ('203.0.113.*','198.51.100.*') LIMIT 1); + + +-- ============================================================================ +-- 11) sys_role_menu — 역할-메뉴 권한 매핑(RBAC 조회 화면 /admin/role-menus) +-- ============================================================================ +-- ADMIN: 전 메뉴 읽기/쓰기. MANAGER: 업무(m-work*) 읽기/쓰기. USER: 업무 읽기전용. +INSERT INTO sys_role_menu (role_code, menu_id, read_yn, write_yn) +SELECT 'ADMIN', id, 'Y', 'Y' FROM sys_menu +ON CONFLICT (role_code, menu_id) DO NOTHING; + +INSERT INTO sys_role_menu (role_code, menu_id, read_yn, write_yn) +SELECT 'MANAGER', id, 'Y', 'Y' FROM sys_menu WHERE id LIKE 'm-work%' +ON CONFLICT (role_code, menu_id) DO NOTHING; + +INSERT INTO sys_role_menu (role_code, menu_id, read_yn, write_yn) +SELECT 'USER', id, 'Y', 'N' FROM sys_menu WHERE id LIKE 'm-work%' +ON CONFLICT (role_code, menu_id) DO NOTHING; + + +-- ============================================================================ +-- 12) audit_log — 감사 로그(관리 조회 화면 /admin/audit) +-- ============================================================================ +-- id 는 bigserial 자동 채번. 재실행 중복 방지 위해 대표 summary 존재 여부로 가드. +INSERT INTO audit_log (actor_id, actor_name, action, target_type, target_id, event_id, summary, ruleset_version, result, ip_hint, created_at) +SELECT * FROM (VALUES + ('demo-organizer','주최자(데모)','AWARD_SELECT','auction','AUC-LIVE-01','e-2026-live','옥션 낙찰 선정 — (주)한빛부스디자인(4,200,000원)', 'auction-v1.0','SUCCESS','203.0.113.*', now()-interval '1 day'), + ('demo-organizer','주최자(데모)','INVOICE_ISSUE','invoice','inv-live-01','e-2026-live','청구서 발행 — (주)스마트오토메이션 7,187,400원', 'billing-v1.0','SUCCESS','203.0.113.*', now()-interval '18 day'), + ('demo-organizer','주최자(데모)','PAYMENT_RECORD','invoice','inv-live-01','e-2026-live','수납 등록 — 잔금 4,312,440원', 'billing-v1.0','SUCCESS','203.0.113.*', now()-interval '5 day'), + ('demo-organizer','주최자(데모)','REFUND_RECORD','invoice','inv-live-08','e-2026-live','환불 산정 — 1,320,000원(개막일 이후 50%)', 'refund-v1.0','SUCCESS','203.0.113.*', now()-interval '2 day'), + ('demo-exhibitor','참가업체(데모)','LEAD_EXPORT','lead',NULL,'e-2026-live','리드 6건 내보내기(CSV)', NULL,'SUCCESS','203.0.113.*', now()-interval '4 hour'), + ('demo-contractor','공사업체(데모)','BID_SUBMIT','auction','AUC-LIVE-02','e-2026-live','응찰 제출 — 3,650,000원', 'auction-v1.0','SUCCESS','198.51.100.*', now()-interval '12 hour'), + ('admin','시스템관리자','ROLE_MENU_UPDATE','sys_role_menu','MANAGER','','역할-메뉴 권한 변경 — MANAGER 업무 메뉴 쓰기 허용', NULL,'SUCCESS','203.0.113.*', now()-interval '10 day'), + ('demo-organizer','주최자(데모)','AWARD_SELECT','auction','AUC-2026-0210','e-2026-smf','옥션 낙찰 선정 — 세계전기시설(12,000,000원)', 'auction-v1.0','SUCCESS','203.0.113.*', TIMESTAMPTZ '2026-07-30 11:00+09') +) AS v(actor_id, actor_name, action, target_type, target_id, event_id, summary, ruleset_version, result, ip_hint, created_at) +WHERE NOT EXISTS (SELECT 1 FROM audit_log WHERE action='AWARD_SELECT' AND target_id='AUC-LIVE-01'); diff --git a/src/backend/src/test/java/com/zioinfo/kintex/system/message/MessageRenderTest.java b/src/backend/src/test/java/com/zioinfo/kintex/system/message/MessageRenderTest.java new file mode 100644 index 0000000..8c01d6e --- /dev/null +++ b/src/backend/src/test/java/com/zioinfo/kintex/system/message/MessageRenderTest.java @@ -0,0 +1,49 @@ +package com.zioinfo.kintex.system.message; + +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * 메시지 템플릿 렌더({key} 치환) 단위테스트 — DB/스프링 없이 renderTemplate 순수 로직 검증(#35). + */ +class MessageRenderTest { + + private final MessageService service = new MessageService(null); + + @Test + void replacesKnownPlaceholders() { + Map p = Map.of("name", "홍길동", "eventName", "스마트팩토리"); + String out = service.renderTemplate("{name}님, {eventName}에 오신 것을 환영합니다.", p); + assertEquals("홍길동님, 스마트팩토리에 오신 것을 환영합니다.", out); + } + + @Test + void keepsMissingPlaceholderAsOriginal() { + String out = service.renderTemplate("{name}님, {eventName} 안내", Map.of("name", "김철수")); + assertEquals("김철수님, {eventName} 안내", out, "누락 파라미터는 원본 {key} 유지"); + } + + @Test + void nullValueBecomesEmpty() { + Map p = new HashMap<>(); + p.put("name", null); + String out = service.renderTemplate("[{name}] 알림", p); + assertEquals("[] 알림", out, "null 값은 빈 문자열로 치환"); + } + + @Test + void nullTemplateAndParamsSafe() { + assertEquals("", service.renderTemplate(null, Map.of("a", "b"))); + assertEquals("고정 문구", service.renderTemplate("고정 문구", null)); + } + + @Test + void ignoresNonPlaceholderBraces() { + String out = service.renderTemplate("금액 {amount} (단위: {}) 원", Map.of("amount", "1,000")); + assertEquals("금액 1,000 (단위: {}) 원", out, "빈 중괄호는 플레이스홀더로 오탐하지 않음"); + } +} diff --git a/src/frontend/src/components/layout/app-shell.css b/src/frontend/src/components/layout/app-shell.css index 93343c7..631c9a6 100644 --- a/src/frontend/src/components/layout/app-shell.css +++ b/src/frontend/src/components/layout/app-shell.css @@ -53,8 +53,8 @@ overflow: hidden; } .kx-shell__brand-sub { - font-size: 11px; - font-weight: 600; + font-size: var(--fs-micro); + font-weight: var(--fw-semibold); line-height: 1.15; color: var(--color-neutral-500); white-space: nowrap; @@ -63,7 +63,7 @@ } .kx-shell__brand-sub--en { font-size: 9px; - font-weight: 500; + font-weight: var(--fw-medium); letter-spacing: 0.02em; color: var(--color-neutral-400, var(--color-neutral-500)); text-transform: uppercase; @@ -84,7 +84,7 @@ } .kx-shell__event-name { font-size: var(--fs-body); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-900); white-space: nowrap; overflow: hidden; @@ -121,8 +121,8 @@ .kx-shell__nav-section { margin: var(--space-2) 0 2px; padding: 4px var(--space-4) 3px; - font-size: 10px; - font-weight: 700; + font-size: var(--fs-nano); + font-weight: var(--fw-bold); letter-spacing: 0.04em; text-transform: uppercase; color: var(--color-neutral-500); @@ -148,7 +148,7 @@ border-radius: var(--radius-sm); color: var(--color-neutral-700); font-size: var(--fs-body); - font-weight: 600; + font-weight: var(--fw-semibold); text-align: left; transition: background 0.15s ease, color 0.15s ease; } @@ -182,7 +182,7 @@ border-radius: var(--radius-sm); color: var(--color-neutral-700); font-size: var(--fs-body); - font-weight: 500; + font-weight: var(--fw-medium); text-decoration: none; transition: background 0.15s ease, color 0.15s ease; } @@ -193,10 +193,10 @@ .kx-shell__nav-link.is-active { background: var(--color-primary-100); color: var(--color-primary-700); - font-weight: 600; + font-weight: var(--fw-semibold); } .kx-shell__nav-link--top { - font-weight: 600; + font-weight: var(--fw-semibold); } /* 스코프 진입(button) — nav-link 와 동일 표현. */ .kx-shell__nav-link--btn { @@ -267,14 +267,14 @@ display: inline-flex; align-items: center; justify-content: center; - font-weight: 600; + font-weight: var(--fw-semibold); font-size: 13px; } .kx-shell__user-name { flex: 1 1 auto; min-width: 0; font-size: var(--fs-body); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-900); white-space: nowrap; overflow: hidden; @@ -376,7 +376,7 @@ } .kx-shell__crumb-cur { color: var(--color-neutral-900); - font-weight: 600; + font-weight: var(--fw-semibold); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -511,12 +511,12 @@ padding: 0; font-family: inherit; font-size: var(--fs-caption); - font-weight: 500; + font-weight: var(--fw-medium); color: var(--color-neutral-500); } .kx-mdi__tab.is-active .kx-mdi__tab-btn { color: var(--color-neutral-900); - font-weight: 600; + font-weight: var(--fw-semibold); } .kx-mdi__tab-label { overflow: hidden; diff --git a/src/frontend/src/components/ui/ai-image.css b/src/frontend/src/components/ui/ai-image.css index 5014c5c..6d635ad 100644 --- a/src/frontend/src/components/ui/ai-image.css +++ b/src/frontend/src/components/ui/ai-image.css @@ -71,7 +71,7 @@ transform: rotate(-24deg); color: rgba(255, 255, 255, 0.55); font-size: 13px; - font-weight: 700; + font-weight: var(--fw-bold); letter-spacing: 0.04em; pointer-events: none; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.25); @@ -92,16 +92,16 @@ color: var(--color-on-accent); border-radius: var(--radius-pill); padding: 2px 8px; - font-size: 11px; - font-weight: 600; + font-size: var(--fs-micro); + font-weight: var(--fw-semibold); } .kx-aiimg__sample { background: rgba(109, 74, 255, 0.9); color: var(--color-on-accent); border-radius: var(--radius-pill); padding: 2px 8px; - font-size: 11px; - font-weight: 700; + font-size: var(--fs-micro); + font-weight: var(--fw-bold); } .kx-aiimg__gen { position: absolute; @@ -116,7 +116,7 @@ border-radius: var(--radius-sm); padding: 6px 12px; font-size: var(--fs-caption); - font-weight: 600; + font-weight: var(--fw-semibold); cursor: pointer; box-shadow: var(--shadow-level2); } @@ -124,7 +124,7 @@ background: var(--color-ai-accent-strong); } .kx-aiimg__notice { - font-size: 11px; + font-size: var(--fs-micro); line-height: 15px; color: var(--color-neutral-500); } diff --git a/src/frontend/src/components/ui/badge.css b/src/frontend/src/components/ui/badge.css index 2f44a10..9b282f4 100644 --- a/src/frontend/src/components/ui/badge.css +++ b/src/frontend/src/components/ui/badge.css @@ -6,7 +6,7 @@ border-radius: var(--radius-pill); font-size: var(--fs-caption); line-height: var(--lh-caption); - font-weight: 600; + font-weight: var(--fw-semibold); white-space: nowrap; } @@ -66,5 +66,5 @@ border: 1px solid var(--color-ai-accent); } .kx-badge__spark { - font-size: 11px; + font-size: var(--fs-micro); } diff --git a/src/frontend/src/components/ui/button.css b/src/frontend/src/components/ui/button.css index a996f04..76b44d5 100644 --- a/src/frontend/src/components/ui/button.css +++ b/src/frontend/src/components/ui/button.css @@ -8,7 +8,7 @@ border-radius: var(--radius-sm); border: 1px solid transparent; font-size: var(--fs-body); - font-weight: 600; + font-weight: var(--fw-semibold); line-height: 1; transition: background-color 0.15s ease, border-color 0.15s ease, opacity 0.15s ease; white-space: nowrap; diff --git a/src/frontend/src/components/ui/compare-slider.css b/src/frontend/src/components/ui/compare-slider.css index a7258bd..8d2cce7 100644 --- a/src/frontend/src/components/ui/compare-slider.css +++ b/src/frontend/src/components/ui/compare-slider.css @@ -29,7 +29,7 @@ padding: 2px 10px; border-radius: var(--radius-pill); font-size: var(--fs-caption); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-on-accent); background: rgba(16, 24, 40, 0.72); pointer-events: none; @@ -64,8 +64,8 @@ border-radius: 50%; background: var(--color-white); color: var(--color-primary-700); - font-size: 14px; - font-weight: 700; + font-size: var(--fs-body); + font-weight: var(--fw-bold); letter-spacing: -1px; box-shadow: var(--shadow-level2); } diff --git a/src/frontend/src/components/ui/offcanvas.css b/src/frontend/src/components/ui/offcanvas.css index 929b0a8..c4d4ef0 100644 --- a/src/frontend/src/components/ui/offcanvas.css +++ b/src/frontend/src/components/ui/offcanvas.css @@ -61,7 +61,7 @@ flex: 1 1 auto; min-width: 0; font-size: var(--fs-h3, var(--fs-body)); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-900); overflow: hidden; text-overflow: ellipsis; diff --git a/src/frontend/src/components/ui/states.css b/src/frontend/src/components/ui/states.css index 8ee125a..c957726 100644 --- a/src/frontend/src/components/ui/states.css +++ b/src/frontend/src/components/ui/states.css @@ -25,7 +25,7 @@ } .kx-state__title { font-size: var(--fs-h3); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-900); } .kx-state__desc { diff --git a/src/frontend/src/i18n/locales/en.json b/src/frontend/src/i18n/locales/en.json index bf3044d..e2ef249 100644 --- a/src/frontend/src/i18n/locales/en.json +++ b/src/frontend/src/i18n/locales/en.json @@ -130,7 +130,7 @@ "contactAddress": "217-60 Kintex-ro, Ilsanseo-gu, Goyang-si, Gyeonggi-do", "contactTel": "Main line 1899-1001", "contactEmail": "exhibition@kintex.com", - "bottom": "© KINTEX. All Rights Reserved. (10390) 217-60, Kintex-ro, Ilsanseo-gu, Goyang-si" + "bottom": "2026 © KINTEX. All Rights Reserved. (10390) 217-60, Kintex-ro, Ilsanseo-gu, Goyang-si" }, "footerbar": { "copyright": "© 2026 KINTEX. All rights reserved.", diff --git a/src/frontend/src/i18n/locales/ja.json b/src/frontend/src/i18n/locales/ja.json index a040be4..7e6808a 100644 --- a/src/frontend/src/i18n/locales/ja.json +++ b/src/frontend/src/i18n/locales/ja.json @@ -130,7 +130,7 @@ "contactAddress": "京畿道高陽市一山西区KINTEXロ217-60", "contactTel": "代表番号 1899-1001", "contactEmail": "exhibition@kintex.com", - "bottom": "© KINTEX. All Rights Reserved. (10390) 217-60, Kintex-ro, Ilsanseo-gu, Goyang-si" + "bottom": "2026 © KINTEX. All Rights Reserved. (10390) 217-60, Kintex-ro, Ilsanseo-gu, Goyang-si" }, "footerbar": { "copyright": "© 2026 KINTEX. All rights reserved.", diff --git a/src/frontend/src/i18n/locales/ko.json b/src/frontend/src/i18n/locales/ko.json index 75419dd..c9ca5e8 100644 --- a/src/frontend/src/i18n/locales/ko.json +++ b/src/frontend/src/i18n/locales/ko.json @@ -130,7 +130,7 @@ "contactAddress": "경기도 고양시 일산서구 킨텍스로 217-60", "contactTel": "대표번호 1899-1001", "contactEmail": "exhibition@kintex.com", - "bottom": "© KINTEX. All Rights Reserved. (10390) 217-60, Kintex-ro, Ilsanseo-gu, Goyang-si" + "bottom": "2026 © KINTEX. All Rights Reserved. (10390) 217-60, Kintex-ro, Ilsanseo-gu, Goyang-si" }, "footerbar": { "copyright": "© 2026 KINTEX. All rights reserved.", diff --git a/src/frontend/src/i18n/locales/zh.json b/src/frontend/src/i18n/locales/zh.json index a2a22af..8e13c43 100644 --- a/src/frontend/src/i18n/locales/zh.json +++ b/src/frontend/src/i18n/locales/zh.json @@ -130,7 +130,7 @@ "contactAddress": "京畿道高阳市一山西区KINTEX路217-60", "contactTel": "总机 1899-1001", "contactEmail": "exhibition@kintex.com", - "bottom": "© KINTEX. 版权所有。(10390) 217-60, Kintex-ro, Ilsanseo-gu, Goyang-si" + "bottom": "2026 © KINTEX. 版权所有。(10390) 217-60, Kintex-ro, Ilsanseo-gu, Goyang-si" }, "footerbar": { "copyright": "© 2026 KINTEX. 版权所有。", diff --git a/src/frontend/src/screens/admin/admin-modules.css b/src/frontend/src/screens/admin/admin-modules.css index ca4697a..45350d8 100644 --- a/src/frontend/src/screens/admin/admin-modules.css +++ b/src/frontend/src/screens/admin/admin-modules.css @@ -15,7 +15,7 @@ .kx-adm__title { font-size: var(--fs-h1); line-height: var(--lh-h1); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-neutral-900); } .kx-adm__subtitle { @@ -34,7 +34,7 @@ } .kx-adm-field__label { font-size: var(--fs-caption); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-500); } .kx-adm-field__input { @@ -60,7 +60,7 @@ padding: 2px 10px; border-radius: var(--radius-pill); font-size: var(--fs-caption); - font-weight: 700; + font-weight: var(--fw-bold); white-space: nowrap; } .kx-adm-pill--success { color: var(--color-success); background: #e6f4ee; } @@ -114,7 +114,7 @@ .kx-adm-audit thead th { background: var(--color-neutral-050); font-size: var(--fs-caption); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-500); letter-spacing: 0.02em; border-bottom: 2px solid var(--color-neutral-200); @@ -132,19 +132,19 @@ border-radius: var(--radius-pill); background: var(--color-primary-100); color: var(--color-primary-700); - font-size: 11px; - font-weight: 700; + font-size: var(--fs-micro); + font-weight: var(--fw-bold); display: inline-flex; align-items: center; justify-content: center; } .kx-adm-actor__body { display: flex; flex-direction: column; } -.kx-adm-actor__name { font-weight: 600; color: var(--color-neutral-900); } +.kx-adm-actor__name { font-weight: var(--fw-semibold); color: var(--color-neutral-900); } .kx-adm-actor__id { font-size: var(--fs-caption); color: var(--color-neutral-500); } .kx-adm-action { display: inline-block; - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-900); } .kx-adm-summary { @@ -158,7 +158,7 @@ white-space: nowrap; } .kx-adm-target { display: flex; flex-direction: column; } -.kx-adm-target__type { font-weight: 600; color: var(--color-neutral-700); } +.kx-adm-target__type { font-weight: var(--fw-semibold); color: var(--color-neutral-700); } .kx-adm-target__id { font-size: var(--fs-caption); color: var(--color-neutral-500); } .kx-adm-pager { @@ -173,7 +173,7 @@ } .kx-adm-pager__info { font-size: var(--fs-caption); color: var(--color-neutral-500); } .kx-adm-pager__nav { display: flex; align-items: center; gap: var(--space-3); } -.kx-adm-pager__page { font-size: var(--fs-body); font-weight: 600; color: var(--color-neutral-700); } +.kx-adm-pager__page { font-size: var(--fs-body); font-weight: var(--fw-semibold); color: var(--color-neutral-700); } .kx-adm-pager__size { display: flex; align-items: center; @@ -193,7 +193,7 @@ .kx-adm-group__body { flex: 1; min-width: 0; padding: var(--space-4); } .kx-adm-group__title { font-size: var(--fs-h3); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-neutral-900); margin-bottom: var(--space-3); text-transform: capitalize; @@ -211,25 +211,25 @@ gap: 6px; flex-wrap: wrap; font-size: var(--fs-body); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-900); font-family: ui-monospace, 'JetBrains Mono', monospace; } .kx-adm-setrow__desc { font-size: var(--fs-caption); color: var(--color-neutral-500); } .kx-adm-setrow__control { display: flex; align-items: center; gap: var(--space-2); } .kx-adm-setrow__input { flex: 1; min-width: 0; } -.kx-adm-setrow__hint { margin-top: 6px; font-size: 11px; color: var(--color-neutral-500); } +.kx-adm-setrow__hint { margin-top: 6px; font-size: var(--fs-micro); color: var(--color-neutral-500); } .kx-adm-secret { - font-size: 10px; - font-weight: 700; + font-size: var(--fs-nano); + font-weight: var(--fw-bold); color: var(--color-warning); background: #fff4e5; padding: 1px 6px; border-radius: var(--radius-pill); } .kx-adm-typechip { - font-size: 10px; - font-weight: 700; + font-size: var(--fs-nano); + font-weight: var(--fw-bold); color: var(--color-neutral-500); background: var(--color-neutral-100); padding: 1px 6px; @@ -265,7 +265,7 @@ .kx-adm-switch input:focus-visible + .kx-adm-switch__track { outline: 2px solid var(--color-primary-100); } .kx-adm-switch__label { font-size: var(--fs-caption); color: var(--color-neutral-700); } -.kx-adm-add__title { font-size: var(--fs-h3); font-weight: 700; color: var(--color-neutral-900); margin-bottom: var(--space-3); } +.kx-adm-add__title { font-size: var(--fs-h3); font-weight: var(--fw-bold); color: var(--color-neutral-900); margin-bottom: var(--space-3); } .kx-adm-add__grid { display: grid; grid-template-columns: 1.4fr 1.4fr 0.8fr; @@ -286,7 +286,7 @@ .kx-adm-ruleset { display: grid; grid-template-columns: 300px 1fr; gap: var(--space-4); align-items: start; } .kx-adm-ruleset__versions { display: flex; flex-direction: column; gap: var(--space-3); } .kx-adm-ruleset__vhead { display: flex; align-items: center; justify-content: space-between; } -.kx-adm-ruleset__vhead h2 { font-size: var(--fs-h3); font-weight: 700; color: var(--color-neutral-900); } +.kx-adm-ruleset__vhead h2 { font-size: var(--fs-h3); font-weight: var(--fw-bold); color: var(--color-neutral-900); } .kx-adm-vlist { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-2); } .kx-adm-vitem { width: 100%; @@ -317,13 +317,13 @@ padding: var(--space-4); border-bottom: var(--border-card); } -.kx-adm-ruleset__ehead h2 { font-size: var(--fs-h3); font-weight: 700; color: var(--color-neutral-900); } +.kx-adm-ruleset__ehead h2 { font-size: var(--fs-h3); font-weight: var(--fw-bold); color: var(--color-neutral-900); } .kx-adm-rules { width: 100%; border-collapse: collapse; } .kx-adm-rules th, .kx-adm-rules td { padding: 10px 12px; text-align: left; font-size: var(--fs-body); } .kx-adm-rules thead th { background: var(--color-neutral-050); font-size: var(--fs-caption); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-500); letter-spacing: 0.02em; border-bottom: 2px solid var(--color-neutral-200); @@ -331,8 +331,8 @@ vertical-align: bottom; } .kx-adm-rules tbody tr { border-bottom: 1px solid var(--color-neutral-200); } -.kx-adm-rulename { display: block; font-weight: 600; color: var(--color-neutral-900); } -.kx-adm-rulecode { display: block; font-size: 11px; color: var(--color-neutral-500); font-family: ui-monospace, monospace; } +.kx-adm-rulename { display: block; font-weight: var(--fw-semibold); color: var(--color-neutral-900); } +.kx-adm-rulecode { display: block; font-size: var(--fs-micro); color: var(--color-neutral-500); font-family: ui-monospace, monospace; } .kx-adm-disclaimer { padding: var(--space-3) var(--space-4); font-size: var(--fs-caption); color: var(--color-neutral-500); border-top: var(--border-card); } /* ═══════════ SCR-A9 테넌트 ═══════════ */ @@ -345,13 +345,13 @@ padding: var(--space-4); border-bottom: var(--border-card); } -.kx-adm-tenant__lhead h2 { font-size: var(--fs-h3); font-weight: 700; color: var(--color-neutral-900); } +.kx-adm-tenant__lhead h2 { font-size: var(--fs-h3); font-weight: var(--fw-bold); color: var(--color-neutral-900); } .kx-adm-tenants { width: 100%; border-collapse: collapse; } .kx-adm-tenants th, .kx-adm-tenants td { padding: 10px 12px; text-align: left; font-size: var(--fs-body); } .kx-adm-tenants thead th { background: var(--color-neutral-050); font-size: var(--fs-caption); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-500); letter-spacing: 0.02em; border-bottom: 2px solid var(--color-neutral-200); @@ -361,10 +361,10 @@ .kx-adm-tenants tbody tr { border-bottom: 1px solid var(--color-neutral-200); } .kx-adm-tenants tbody tr:hover { background: var(--color-primary-050); } .kx-adm-tenants tbody tr.is-selected { background: var(--color-primary-050); } -.kx-adm-tenants__name { font-weight: 600; color: var(--color-neutral-900); } +.kx-adm-tenants__name { font-weight: var(--fw-semibold); color: var(--color-neutral-900); } .kx-adm-tenant__wizard { display: flex; flex-direction: column; gap: 4px; } -.kx-adm-wiz__title { font-size: var(--fs-h3); font-weight: 700; color: var(--color-neutral-900); } +.kx-adm-wiz__title { font-size: var(--fs-h3); font-weight: var(--fw-bold); color: var(--color-neutral-900); } .kx-adm-wiz { list-style: none; margin: var(--space-3) 0 0; padding: 0; display: flex; flex-direction: column; } .kx-adm-wiz__step { display: flex; gap: var(--space-3); padding-bottom: var(--space-4); position: relative; } .kx-adm-wiz__step:not(:last-child)::before { @@ -385,7 +385,7 @@ align-items: center; justify-content: center; font-size: var(--fs-caption); - font-weight: 700; + font-weight: var(--fw-bold); background: var(--color-neutral-100); color: var(--color-neutral-500); z-index: 1; @@ -393,7 +393,7 @@ .kx-adm-wiz__step.is-done .kx-adm-wiz__dot { background: var(--color-success); color: var(--color-on-accent); } .kx-adm-wiz__step.is-current .kx-adm-wiz__dot { background: var(--color-primary-600); color: var(--color-on-accent); } .kx-adm-wiz__body { display: flex; flex-direction: column; gap: 2px; padding-top: 3px; } -.kx-adm-wiz__label { font-size: var(--fs-body); font-weight: 600; color: var(--color-neutral-900); } +.kx-adm-wiz__label { font-size: var(--fs-body); font-weight: var(--fw-semibold); color: var(--color-neutral-900); } .kx-adm-wiz__step.is-todo .kx-adm-wiz__label { color: var(--color-neutral-500); } .kx-adm-wiz__desc { font-size: var(--fs-caption); color: var(--color-neutral-500); } @@ -422,7 +422,7 @@ .kx-adm-tbl thead th { background: var(--color-neutral-050); font-size: var(--fs-caption); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-500); letter-spacing: 0.02em; border-bottom: 2px solid var(--color-neutral-200); @@ -432,7 +432,7 @@ .kx-adm-tbl tbody tr { border-bottom: 1px solid var(--color-neutral-200); } .kx-adm-tbl tbody tr:hover { background: var(--color-primary-050); } .kx-adm-tbl tbody tr.is-selected { background: var(--color-primary-050); } -.kx-adm-tbl__strong { font-weight: 600; color: var(--color-neutral-900); } +.kx-adm-tbl__strong { font-weight: var(--fw-semibold); color: var(--color-neutral-900); } .kx-adm-tbl__actions { display: flex; gap: var(--space-2); justify-content: flex-end; white-space: nowrap; } /* 역할 칩 */ @@ -442,7 +442,7 @@ padding: 2px 8px; border-radius: var(--radius-pill); font-size: var(--fs-caption); - font-weight: 700; + font-weight: var(--fw-bold); } .kx-adm-rolechip--admin { color: var(--color-error); background: #fef3f2; } .kx-adm-rolechip--manager { color: var(--color-primary-700); background: var(--color-primary-100); } @@ -475,7 +475,7 @@ box-shadow: var(--shadow-modal, 0 20px 48px rgba(15, 23, 42, 0.24)); padding: var(--space-5, 24px); } -.kx-adm-modal__title { font-size: var(--fs-h2); font-weight: 700; color: var(--color-neutral-900); } +.kx-adm-modal__title { font-size: var(--fs-h2); font-weight: var(--fw-bold); color: var(--color-neutral-900); } .kx-adm-modal__desc { margin-top: 4px; font-size: var(--fs-caption); color: var(--color-neutral-500); } .kx-adm-modal__grid { margin-top: var(--space-4); @@ -510,7 +510,7 @@ } .kx-adm-secnote { margin-top: var(--space-3); - font-size: 11px; + font-size: var(--fs-micro); color: var(--color-neutral-500); } @@ -535,8 +535,8 @@ } .kx-adm-permcheck input { margin-top: 3px; } .kx-adm-permcheck__body { display: flex; flex-direction: column; gap: 2px; min-width: 0; } -.kx-adm-permcheck__name { font-weight: 600; color: var(--color-neutral-900); font-size: var(--fs-body); } -.kx-adm-permcheck__code { font-size: 11px; color: var(--color-neutral-500); font-family: ui-monospace, monospace; } +.kx-adm-permcheck__name { font-weight: var(--fw-semibold); color: var(--color-neutral-900); font-size: var(--fs-body); } +.kx-adm-permcheck__code { font-size: var(--fs-micro); color: var(--color-neutral-500); font-family: ui-monospace, monospace; } .kx-adm-permcheck__desc { font-size: var(--fs-caption); color: var(--color-neutral-500); } /* ── 공통코드 2단(그룹 → 코드) ── */ @@ -550,7 +550,7 @@ padding: var(--space-4); border-bottom: var(--border-card); } -.kx-adm-codes__head h2 { font-size: var(--fs-h3); font-weight: 700; color: var(--color-neutral-900); } +.kx-adm-codes__head h2 { font-size: var(--fs-h3); font-weight: var(--fw-bold); color: var(--color-neutral-900); } .kx-adm-glist { list-style: none; margin: 0; padding: var(--space-2); display: flex; flex-direction: column; gap: 4px; } .kx-adm-gitem { width: 100%; @@ -567,8 +567,8 @@ } .kx-adm-gitem:hover { background: var(--color-neutral-050); } .kx-adm-gitem.is-active { border-left-color: var(--color-primary-600); background: var(--color-primary-050); } -.kx-adm-gitem__name { font-weight: 600; color: var(--color-neutral-900); } -.kx-adm-gitem__code { font-size: 11px; color: var(--color-neutral-500); font-family: ui-monospace, monospace; } +.kx-adm-gitem__name { font-weight: var(--fw-semibold); color: var(--color-neutral-900); } +.kx-adm-gitem__code { font-size: var(--fs-micro); color: var(--color-neutral-500); font-family: ui-monospace, monospace; } /* ── 메뉴 트리 ── */ .kx-adm-menu__name { display: inline-flex; align-items: center; gap: 6px; } @@ -586,7 +586,7 @@ max-width: 480px; } .kx-adm-denied__icon { color: var(--color-error); } -.kx-adm-denied__title { font-size: var(--fs-h1); font-weight: 700; color: var(--color-neutral-900); } +.kx-adm-denied__title { font-size: var(--fs-h1); font-weight: var(--fw-bold); color: var(--color-neutral-900); } .kx-adm-denied__desc { font-size: var(--fs-body); color: var(--color-neutral-500); } .kx-adm-denied__code { margin-top: var(--space-3); font-size: var(--fs-caption); font-family: ui-monospace, monospace; color: var(--color-neutral-400, #94a3b8); } @@ -632,18 +632,18 @@ .kx-adm-health-card--error { border-left-color: var(--color-error); } .kx-adm-health-card--neutral { border-left-color: var(--color-warning); } .kx-adm-health-card__label { font-size: var(--fs-caption); color: var(--color-neutral-500); } -.kx-adm-health-card__value { font-size: var(--fs-h2); font-weight: 700; color: var(--color-neutral-900); } +.kx-adm-health-card__value { font-size: var(--fs-h2); font-weight: var(--fw-bold); color: var(--color-neutral-900); } .kx-adm-health-card__sub { font-size: var(--fs-caption); color: var(--color-neutral-500); } .kx-adm-metrics { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--space-4); } .kx-adm-metric { display: flex; flex-direction: column; gap: 4px; } .kx-adm-metric__label { font-size: var(--fs-caption); color: var(--color-neutral-500); } -.kx-adm-metric__value { font-size: var(--fs-h3); font-weight: 700; color: var(--color-neutral-900); } +.kx-adm-metric__value { font-size: var(--fs-h3); font-weight: var(--fw-bold); color: var(--color-neutral-900); } .kx-adm-metric__sub { font-size: var(--fs-caption); color: var(--color-neutral-500); } /* 폼(인증정책·설정) */ .kx-adm-form { display: grid; gap: var(--space-3); } .kx-adm-form__section { - font-size: var(--fs-h3); font-weight: 700; color: var(--color-neutral-900); + font-size: var(--fs-h3); font-weight: var(--fw-bold); color: var(--color-neutral-900); margin: var(--space-2) 0 0; padding-top: var(--space-2); border-top: 1px solid var(--color-neutral-100); } .kx-adm-form__section:first-child { border-top: none; padding-top: 0; } diff --git a/src/frontend/src/screens/admin/admin.css b/src/frontend/src/screens/admin/admin.css index 30f203b..88a68aa 100644 --- a/src/frontend/src/screens/admin/admin.css +++ b/src/frontend/src/screens/admin/admin.css @@ -37,7 +37,7 @@ align-items: center; gap: 6px; font-size: var(--fs-caption); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-success); } .kx-admin__status-dot { @@ -133,14 +133,14 @@ 50% { box-shadow: 0 0 0 5px rgba(217, 45, 32, 0); } } .kx-livecard__state { - font-size: 11px; - font-weight: 700; + font-size: var(--fs-micro); + font-weight: var(--fw-bold); letter-spacing: 0.02em; color: var(--color-neutral-500); } .kx-livecard__title { font-size: var(--fs-body); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-900); } .kx-livecard__meta { @@ -192,7 +192,7 @@ } .kx-slideadm__label { font-size: var(--fs-caption); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-700); } .kx-slideadm__upload input[type='file'] { @@ -233,7 +233,7 @@ } .kx-slideadm__count { font-size: var(--fs-caption); - font-weight: 500; + font-weight: var(--fw-medium); color: var(--color-neutral-500); margin-left: var(--space-2); } @@ -292,8 +292,8 @@ flex-shrink: 0; padding: 2px 8px; border-radius: var(--radius-pill); - font-size: 11px; - font-weight: 600; + font-size: var(--fs-micro); + font-weight: var(--fw-semibold); } .kx-slideadm__live-badge { background: rgba(0, 102, 179, 0.1); @@ -313,7 +313,7 @@ white-space: nowrap; } .kx-slideadm__row-meta { - font-size: 11px; + font-size: var(--fs-micro); color: var(--color-neutral-500); margin-top: 2px; } diff --git a/src/frontend/src/screens/admin/dept-admin.css b/src/frontend/src/screens/admin/dept-admin.css index 7549ac9..1d77d5d 100644 --- a/src/frontend/src/screens/admin/dept-admin.css +++ b/src/frontend/src/screens/admin/dept-admin.css @@ -134,10 +134,10 @@ text-overflow: ellipsis; white-space: nowrap; font-size: var(--fs-body); - font-weight: 500; + font-weight: var(--fw-medium); } .kx-dept-tree__row.is-selected .kx-dept-tree__name { - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-primary-700); } .kx-dept-tree__sort { @@ -188,7 +188,7 @@ .kx-dept-detail__headMain { min-width: 0; } .kx-dept-detail__title { font-size: var(--fs-h2); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-neutral-900); margin: 0; } diff --git a/src/frontend/src/screens/analytics/aiConsole.css b/src/frontend/src/screens/analytics/aiConsole.css index b3cbfb0..375d379 100644 --- a/src/frontend/src/screens/analytics/aiConsole.css +++ b/src/frontend/src/screens/analytics/aiConsole.css @@ -81,7 +81,7 @@ } .kx-aicon__ungrounded { color: #B54708; - font-weight: 600; + font-weight: var(--fw-semibold); } .kx-aicon__degraded { color: var(--color-neutral-500); @@ -118,7 +118,7 @@ } .kx-aicon__cites-h { display: block; - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-700, #344054); margin-bottom: var(--space-1, 4px); } diff --git a/src/frontend/src/screens/analytics/analytics.css b/src/frontend/src/screens/analytics/analytics.css index 3390c6a..1337eb8 100644 --- a/src/frontend/src/screens/analytics/analytics.css +++ b/src/frontend/src/screens/analytics/analytics.css @@ -91,7 +91,7 @@ } .kx-bi__global-title { font-size: var(--fs-h3, 1.125rem); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-neutral-900); } /* 전역 섹션의 하위 split은 동일 폭 2컬럼 */ diff --git a/src/frontend/src/screens/appqr/appqr.css b/src/frontend/src/screens/appqr/appqr.css index b65ad18..a431f15 100644 --- a/src/frontend/src/screens/appqr/appqr.css +++ b/src/frontend/src/screens/appqr/appqr.css @@ -76,12 +76,12 @@ } .kx-appqr__name { font-size: var(--fs-h3); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-neutral-900); } .kx-appqr__ver { font-size: var(--fs-caption); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-on-accent); background: var(--color-primary-600); border-radius: var(--radius-pill); @@ -145,7 +145,7 @@ background: var(--color-primary-050); color: var(--color-primary-700); font-size: var(--fs-caption); - font-weight: 700; + font-weight: var(--fw-bold); } .kx-appqr__step-text { font-size: var(--fs-body); diff --git a/src/frontend/src/screens/approval/ManagerApprovalQueuePage.tsx b/src/frontend/src/screens/approval/ManagerApprovalQueuePage.tsx index ebefeaa..59e86c3 100644 --- a/src/frontend/src/screens/approval/ManagerApprovalQueuePage.tsx +++ b/src/frontend/src/screens/approval/ManagerApprovalQueuePage.tsx @@ -185,7 +185,7 @@ export function ManagerApprovalQueuePage() { ) : rows.length === 0 ? ( ) : ( - +
diff --git a/src/frontend/src/screens/approval/approval-form.css b/src/frontend/src/screens/approval/approval-form.css index f1aa746..7d1c0c5 100644 --- a/src/frontend/src/screens/approval/approval-form.css +++ b/src/frontend/src/screens/approval/approval-form.css @@ -16,7 +16,7 @@ } .kx-apf__head h1 { font-size: var(--fs-h3); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-neutral-900); } .kx-apf__back { @@ -64,7 +64,7 @@ .kx-apf__reject-head strong { font-size: var(--fs-caption); color: #b91c1c; - font-weight: 700; + font-weight: var(--fw-bold); } .kx-apf__reject-body { font-size: var(--fs-body); @@ -88,7 +88,7 @@ font-size: var(--fs-caption); } .kx-apf__prev-act { - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-neutral-800); } .kx-apf__prev-meta { @@ -125,7 +125,7 @@ } .kx-apf__label { font-size: var(--fs-caption); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-700); } .kx-apf__req { @@ -192,7 +192,7 @@ min-width: 52px; text-align: center; font-size: var(--fs-caption); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-primary-700); background: var(--color-primary-050); border-radius: var(--radius-pill); @@ -260,7 +260,7 @@ border-radius: var(--radius-sm); padding: 6px 14px; font-size: var(--fs-caption); - font-weight: 600; + font-weight: var(--fw-semibold); cursor: pointer; } .kx-line-editor__add:hover:not(:disabled) { @@ -300,7 +300,7 @@ } .kx-apm__head h2 { font-size: var(--fs-h3); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-neutral-900); } .kx-apm__close { @@ -318,7 +318,7 @@ } .kx-apm__label { font-size: var(--fs-caption); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-700); } .kx-apm__textarea { @@ -334,7 +334,7 @@ border-color: var(--color-primary-600); } .kx-apm__hint { - font-size: 11px; + font-size: var(--fs-micro); color: var(--color-neutral-500); } .kx-apm__foot { diff --git a/src/frontend/src/screens/approval/approval-queue.css b/src/frontend/src/screens/approval/approval-queue.css index c2868ad..453d7ca 100644 --- a/src/frontend/src/screens/approval/approval-queue.css +++ b/src/frontend/src/screens/approval/approval-queue.css @@ -25,7 +25,7 @@ } .kx-aq__title-row h1 { font-size: var(--fs-h3); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-neutral-900); } .kx-aq__hall-tabs { @@ -45,7 +45,7 @@ border-color: var(--color-primary-600); background: var(--color-primary-050); color: var(--color-primary-700); - font-weight: 700; + font-weight: var(--fw-bold); } .kx-aq__search { display: flex; @@ -123,43 +123,16 @@ overflow: auto; background: var(--color-white); } -.kx-aq__table { - width: 100%; - border-collapse: collapse; - font-size: var(--fs-body); -} -.kx-aq__table thead { - position: sticky; - top: 0; -} -.kx-aq__table th { - text-align: left; - padding: 10px 12px; - font-size: var(--fs-caption); - color: var(--color-neutral-500); - font-weight: 600; - letter-spacing: 0.02em; - background: var(--color-neutral-050); - border-bottom: 2px solid var(--color-neutral-200); - white-space: nowrap; - vertical-align: bottom; -} +/* 표는 정본 .kx-table 로 수렴(approval-queue.css 자체 테이블 정의 제거). + * 아래는 셀/컬럼 modifier 만 유지. */ .kx-aq__col-ai { color: var(--color-ai-accent) !important; } .kx-aq__col-act { text-align: right; } -.kx-aq__table td { - padding: 10px 12px; - border-bottom: 1px solid var(--color-neutral-200); - vertical-align: middle; -} -.kx-aq__table tbody tr:hover { - background: var(--row-hover); -} .kx-aq__event { - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-neutral-900); } .kx-aq__booth { @@ -173,8 +146,8 @@ display: inline-flex; align-items: center; gap: 4px; - font-size: 11px; - font-weight: 700; + font-size: var(--fs-micro); + font-weight: var(--fw-bold); padding: 3px 8px; border-radius: var(--radius-sm); } @@ -195,7 +168,7 @@ border-radius: var(--radius-sm); padding: 5px 14px; font-size: var(--fs-caption); - font-weight: 600; + font-weight: var(--fw-semibold); cursor: pointer; } .kx-aq__review:hover { @@ -262,7 +235,7 @@ .kx-aq__load-scale { display: flex; justify-content: space-between; - font-size: 11px; + font-size: var(--fs-micro); color: var(--color-neutral-500); } .kx-aq__card--ai { @@ -318,7 +291,7 @@ } .kx-aq__tab.is-active { color: var(--color-primary-700); - font-weight: 700; + font-weight: var(--fw-bold); border-bottom-color: var(--color-primary-600); } .kx-aq__notice { @@ -331,7 +304,7 @@ } .kx-aq__count { font-size: var(--fs-caption); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-primary-600); background: var(--color-primary-050); border-radius: var(--radius-pill); @@ -350,8 +323,8 @@ align-items: center; padding: 2px 10px; border-radius: 999px; - font-size: 12px; - font-weight: 600; + font-size: var(--fs-caption); + font-weight: var(--fw-semibold); background: rgba(148, 163, 184, 0.16); color: #475569; } diff --git a/src/frontend/src/screens/approval/review-details.css b/src/frontend/src/screens/approval/review-details.css index 7f33a05..6dd970f 100644 --- a/src/frontend/src/screens/approval/review-details.css +++ b/src/frontend/src/screens/approval/review-details.css @@ -45,7 +45,7 @@ color: var(--color-neutral-900); } .kx-rev__meta { - font-size: 11px; + font-size: var(--fs-micro); color: var(--color-neutral-500); background: var(--color-neutral-100); border: 1px solid var(--color-neutral-200); @@ -186,7 +186,7 @@ display: flex; align-items: center; justify-content: center; - font-weight: 700; + font-weight: var(--fw-bold); font-size: var(--fs-caption); border: 2px solid rgba(255, 255, 255, 0.3); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); @@ -244,8 +244,8 @@ border-bottom: 1px solid var(--color-neutral-200); } .kx-rev__ai-count { - font-size: 11px; - font-weight: 700; + font-size: var(--fs-micro); + font-weight: var(--fw-bold); color: var(--color-error); background: #fef3f2; padding: 2px 8px; @@ -277,13 +277,13 @@ display: flex; align-items: center; justify-content: center; - font-weight: 700; - font-size: 12px; + font-weight: var(--fw-bold); + font-size: var(--fs-caption); flex-shrink: 0; } .kx-rev__vtitle { font-size: var(--fs-body); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-900); margin-bottom: 2px; } @@ -321,8 +321,8 @@ display: flex; align-items: center; justify-content: center; - font-size: 11px; - font-weight: 700; + font-size: var(--fs-micro); + font-weight: var(--fw-bold); flex-shrink: 0; } .kx-rev__avatar--con { @@ -340,7 +340,7 @@ color: var(--color-neutral-900); } .kx-rev__comment-top span { - font-size: 11px; + font-size: var(--fs-micro); color: var(--color-neutral-500); } .kx-rev__comment p { @@ -407,8 +407,8 @@ display: inline-flex; align-items: center; justify-content: center; - font-size: 12px; - font-weight: 700; + font-size: var(--fs-caption); + font-weight: var(--fw-bold); background: rgba(148, 163, 184, 0.2); color: #475569; } diff --git a/src/frontend/src/screens/auction/AuctionDetailPage.tsx b/src/frontend/src/screens/auction/AuctionDetailPage.tsx index bff805e..73f907a 100644 --- a/src/frontend/src/screens/auction/AuctionDetailPage.tsx +++ b/src/frontend/src/screens/auction/AuctionDetailPage.tsx @@ -425,13 +425,13 @@ function BoqPanel({ ) : ( <>
-
{t('approval.colTitle')}
+
- + @@ -444,7 +444,7 @@ function BoqPanel({ {l.trade} - + diff --git a/src/frontend/src/screens/auction/auction.css b/src/frontend/src/screens/auction/auction.css index aff7569..75c65b0 100644 --- a/src/frontend/src/screens/auction/auction.css +++ b/src/frontend/src/screens/auction/auction.css @@ -16,7 +16,7 @@ font-size: var(--fs-h1); line-height: var(--lh-h1); color: var(--color-neutral-900); - font-weight: 700; + font-weight: var(--fw-bold); } .kx-auc__subtitle { font-size: var(--fs-body); @@ -35,8 +35,8 @@ display: inline-flex; align-items: center; gap: 6px; - font-size: 11px; - font-weight: 600; + font-size: var(--fs-micro); + font-weight: var(--fw-semibold); color: var(--color-warning); background: #fff4e5; padding: 3px 10px; @@ -119,7 +119,7 @@ .kx-auc-card__name { font-size: var(--fs-h3); line-height: var(--lh-h3); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-neutral-900); } .kx-auc-card__status { @@ -152,7 +152,7 @@ } .kx-auc-card__metric-value { font-size: var(--fs-h3); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-neutral-900); } .kx-auc-card__metric-value--price { @@ -176,7 +176,7 @@ gap: 4px; font-size: var(--fs-caption); color: var(--color-ai-accent); - font-weight: 600; + font-weight: var(--fw-semibold); } /* 카테고리·유형 태그 */ @@ -186,7 +186,7 @@ padding: 2px 10px; border-radius: var(--radius-pill); font-size: var(--fs-caption); - font-weight: 600; + font-weight: var(--fw-semibold); background: var(--color-primary-100); color: var(--color-primary-700); } @@ -206,7 +206,7 @@ padding: 2px 10px; border-radius: var(--radius-sm); font-size: var(--fs-caption); - font-weight: 700; + font-weight: var(--fw-bold); } .kx-auc-pill--live { background: #e6f4ee; @@ -237,7 +237,7 @@ } .kx-auc-open__head h2 { font-size: var(--fs-h3); - font-weight: 700; + font-weight: var(--fw-bold); display: flex; align-items: center; gap: 8px; @@ -265,15 +265,15 @@ .kx-auc .kx-field__label { font-size: var(--fs-caption); color: var(--color-neutral-700); - font-weight: 600; + font-weight: var(--fw-semibold); display: flex; align-items: center; justify-content: space-between; } .kx-auc .kx-field__label-note { - font-size: 11px; + font-size: var(--fs-micro); color: var(--color-error); - font-weight: 500; + font-weight: var(--fw-medium); } .kx-auc .kx-input { height: 36px; @@ -321,7 +321,7 @@ justify-content: space-between; font-size: var(--fs-caption); color: var(--color-ai-accent); - font-weight: 600; + font-weight: var(--fw-semibold); margin-bottom: 4px; } .kx-weight input[type='range'] { @@ -343,12 +343,12 @@ border-radius: var(--radius-lg); color: var(--color-ai-accent); cursor: pointer; - font-weight: 600; + font-weight: var(--fw-semibold); } .kx-auc-attach small { color: var(--color-neutral-500); - font-weight: 400; - font-size: 11px; + font-weight: var(--fw-regular); + font-size: var(--fs-micro); } /* 초대 업체 리스트 */ @@ -374,9 +374,9 @@ color: var(--color-neutral-500); } .kx-invite__blocked-tag { - font-size: 11px; + font-size: var(--fs-micro); color: var(--color-error); - font-weight: 600; + font-weight: var(--fw-semibold); } /* ── SCR-27 상세 레이아웃 ── */ @@ -404,7 +404,7 @@ background: #fef3f2; color: var(--color-error); font-size: var(--fs-caption); - font-weight: 700; + font-weight: var(--fw-bold); padding: 3px 10px; border-radius: var(--radius-sm); } @@ -435,14 +435,14 @@ align-items: flex-end; } .kx-aucd__metric-label { - font-size: 11px; + font-size: var(--fs-micro); text-transform: uppercase; letter-spacing: 0.04em; color: var(--color-neutral-500); } .kx-aucd__metric-value { font-size: var(--fs-h3); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-primary-700); } .kx-aucd__metric-value--ok { @@ -481,7 +481,7 @@ border: none; background: transparent; font-size: var(--fs-body); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-500); cursor: pointer; } @@ -518,7 +518,7 @@ .kx-viewer__placeholder-title { font-size: var(--fs-h3); color: rgba(255, 255, 255, 0.85); - font-weight: 600; + font-weight: var(--fw-semibold); } .kx-viewer__placeholder-desc { font-size: var(--fs-caption); @@ -529,7 +529,7 @@ position: absolute; right: 16px; bottom: 12px; - font-size: 11px; + font-size: var(--fs-micro); font-style: italic; color: rgba(255, 255, 255, 0.4); pointer-events: none; @@ -556,7 +556,7 @@ align-items: center; gap: 6px; font-size: var(--fs-h3); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-neutral-900); } .kx-toggle { @@ -624,7 +624,7 @@ display: flex; align-items: center; justify-content: center; - font-weight: 700; + font-weight: var(--fw-bold); background: var(--color-primary-100); color: var(--color-primary-700); flex-shrink: 0; @@ -639,7 +639,7 @@ } .kx-rank__alias { font-size: var(--fs-body); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-neutral-900); } .kx-rank__row--me .kx-rank__alias { @@ -647,21 +647,21 @@ } .kx-rank__price { font-size: var(--fs-h3); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-primary-700); } .kx-rank__price--masked { font-size: var(--fs-body); color: var(--color-neutral-500); - font-weight: 600; + font-weight: var(--fw-semibold); } .kx-rank__tag { - font-size: 11px; + font-size: var(--fs-micro); padding: 1px 8px; border-radius: var(--radius-pill); background: #e6f4ee; color: var(--color-success); - font-weight: 600; + font-weight: var(--fw-semibold); white-space: nowrap; } @@ -683,21 +683,21 @@ background: var(--color-white); } .kx-countdown__label { - font-size: 11px; + font-size: var(--fs-micro); text-transform: uppercase; letter-spacing: 0.16em; color: var(--color-neutral-500); - font-weight: 700; + font-weight: var(--fw-bold); } .kx-countdown__value { font-size: var(--fs-display); line-height: var(--lh-display); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-primary-700); font-variant-numeric: tabular-nums; } .kx-rank__note { - font-size: 11px; + font-size: var(--fs-micro); color: var(--color-neutral-500); text-align: center; line-height: 1.5; @@ -723,12 +723,12 @@ border-left-color: var(--color-success); } .kx-award__chip-label { - font-size: 11px; + font-size: var(--fs-micro); color: var(--color-neutral-500); } .kx-award__chip-value { font-size: var(--fs-body); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-neutral-900); } @@ -764,14 +764,14 @@ } .kx-compare thead th { background: var(--color-neutral-050); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-neutral-900); } .kx-compare thead th.kx-compare__metric-col, .kx-compare td.kx-compare__metric-col { text-align: left; width: 22%; - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-700); } .kx-compare__co { @@ -779,7 +779,7 @@ } .kx-compare__co-name { font-size: var(--fs-h3); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-neutral-900); } .kx-compare__co--rec .kx-compare__co-name { @@ -787,16 +787,16 @@ } .kx-compare__co-sub { display: block; - font-size: 11px; - font-weight: 400; + font-size: var(--fs-micro); + font-weight: var(--fw-regular); color: var(--color-neutral-500); margin-top: 2px; } .kx-compare__rec-tag { display: inline-block; margin-bottom: 6px; - font-size: 10px; - font-weight: 700; + font-size: var(--fs-nano); + font-weight: var(--fw-bold); color: var(--color-on-accent); background: var(--color-primary-700); padding: 2px 8px; @@ -805,15 +805,15 @@ .kx-compare__best { background: #f0fbf6; color: var(--color-success); - font-weight: 700; + font-weight: var(--fw-bold); } .kx-compare__val-strong { font-size: var(--fs-h3); - font-weight: 700; + font-weight: var(--fw-bold); } .kx-compare__note { display: block; - font-size: 11px; + font-size: var(--fs-micro); color: var(--color-neutral-500); margin-top: 2px; } @@ -825,7 +825,7 @@ } .kx-compare__score-val { font-size: var(--fs-display); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-ai-accent); } .kx-compare__star { @@ -857,7 +857,7 @@ } .kx-risk__grade-value { font-size: 40px; - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-ai-accent); line-height: 1.1; } @@ -882,7 +882,7 @@ } .kx-award__panel h2 { font-size: var(--fs-h2); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-primary-700); } .kx-award__rec { @@ -907,11 +907,11 @@ align-items: center; justify-content: center; font-size: var(--fs-h2); - font-weight: 700; + font-weight: var(--fw-bold); } .kx-award__rec-name { font-size: var(--fs-h3); - font-weight: 700; + font-weight: var(--fw-bold); } .kx-award__rec-ai { display: flex; @@ -931,12 +931,12 @@ display: block; } .kx-award__rec-cell-label { - font-size: 11px; + font-size: var(--fs-micro); color: var(--color-neutral-500); } .kx-award__rec-cell-value { font-size: var(--fs-body); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-neutral-900); } .kx-award__textarea { @@ -955,7 +955,7 @@ padding: var(--space-3); background: #fef3f2; border-radius: var(--radius-sm); - font-size: 11px; + font-size: var(--fs-micro); line-height: 1.5; color: var(--color-error); } @@ -965,7 +965,7 @@ } .kx-award__steps-title { font-size: var(--fs-caption); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-neutral-700); margin-bottom: var(--space-3); } @@ -992,8 +992,8 @@ display: flex; align-items: center; justify-content: center; - font-size: 11px; - font-weight: 700; + font-size: var(--fs-micro); + font-weight: var(--fw-bold); flex-shrink: 0; } @@ -1036,7 +1036,7 @@ } .kx-boq__title { font-size: var(--fs-h3); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-text-strong); } .kx-boq__scope { @@ -1048,46 +1048,22 @@ border: var(--border-card); border-radius: var(--radius-lg); } -.kx-boq__table { - width: 100%; - border-collapse: collapse; - font-size: var(--fs-body); - color: var(--color-neutral-700); -} -.kx-boq__table th, -.kx-boq__table td { - padding: 10px 12px; - border-bottom: 1px solid var(--color-neutral-200); - text-align: left; +/* BOQ 표는 정본 .kx-table 로 수렴. 셀 상단정렬(다행 항목)만 스코프 유지. */ +.kx-boq__scroll .kx-table td { vertical-align: top; } -.kx-boq__table thead th { - background: var(--color-neutral-050); - font-size: var(--fs-caption); - font-weight: 600; - letter-spacing: 0.02em; - color: var(--color-neutral-500); - border-bottom: 2px solid var(--color-neutral-200); - white-space: nowrap; - vertical-align: bottom; - position: sticky; - top: 0; -} -.kx-boq__num { - text-align: right; -} .kx-boq__trade { display: inline-block; padding: 2px 8px; border-radius: 999px; background: var(--color-primary-surface, #eef2ff); color: var(--color-primary-600, #2f42fc); - font-size: 11px; - font-weight: 600; + font-size: var(--fs-micro); + font-weight: var(--fw-semibold); } .kx-boq__basis { color: var(--color-text-muted); - font-size: 11px; + font-size: var(--fs-micro); } .kx-boq__note { font-size: var(--fs-caption); diff --git a/src/frontend/src/screens/boothsales/boothsales.css b/src/frontend/src/screens/boothsales/boothsales.css index 3c5be3d..588c2e6 100644 --- a/src/frontend/src/screens/boothsales/boothsales.css +++ b/src/frontend/src/screens/boothsales/boothsales.css @@ -16,8 +16,9 @@ flex-wrap: wrap; } .kx-bsales__title { - font-size: 20px; - font-weight: 700; + font-size: var(--fs-h1); + line-height: var(--lh-h1); + font-weight: var(--fw-bold); margin: 0; } .kx-bsales__subtitle { @@ -38,18 +39,18 @@ gap: 6px; padding: 6px 12px; border-radius: 999px; - font-size: 12px; + font-size: var(--fs-caption); background: var(--kx-surface-2, #f1f5f9); border: 1px solid var(--kx-border, #e2e8f0); } -.kx-bsales__stat strong { font-size: 15px; font-weight: 700; } +.kx-bsales__stat strong { font-size: 15px; font-weight: var(--fw-bold); } .kx-bsales__stat.is-available { color: #15803d; background: #f0fdf4; border-color: #bbf7d0; } .kx-bsales__stat.is-held { color: #b45309; background: #fffbeb; border-color: #fde68a; } .kx-bsales__stat.is-sold { color: #475569; background: #f1f5f9; border-color: #cbd5e1; } .kx-bsales__stat.is-blocked { color: #b91c1c; background: #fef2f2; border-color: #fecaca; } .kx-bsales__rate { margin-left: auto; - font-size: 12px; + font-size: var(--fs-caption); color: var(--kx-text-muted, #64748b); } @@ -94,14 +95,14 @@ display: inline-flex; align-items: center; gap: 5px; - font-weight: 600; + font-weight: var(--fw-semibold); font-size: 13px; } .kx-boothstatus { - font-size: 11px; + font-size: var(--fs-micro); padding: 2px 8px; border-radius: 999px; - font-weight: 600; + font-weight: var(--fw-semibold); } .kx-boothstatus.is-available { color: #15803d; background: #f0fdf4; } .kx-boothstatus.is-held { color: #b45309; background: #fffbeb; } @@ -110,10 +111,10 @@ .kx-boothcard__price { font-size: 22px; - font-weight: 700; + font-weight: var(--fw-bold); line-height: 1.1; } -.kx-boothcard__won { font-size: 13px; font-weight: 500; color: var(--kx-text-muted, #64748b); margin-left: 2px; } +.kx-boothcard__won { font-size: 13px; font-weight: var(--fw-medium); color: var(--kx-text-muted, #64748b); margin-left: 2px; } .kx-boothcard__meta { list-style: none; @@ -122,7 +123,7 @@ display: flex; flex-direction: column; gap: 3px; - font-size: 12px; + font-size: var(--fs-caption); color: var(--kx-text-muted, #64748b); } @@ -134,5 +135,5 @@ } .kx-boothcard__actions .kx-btn { padding: 5px 10px; - font-size: 12px; + font-size: var(--fs-caption); } diff --git a/src/frontend/src/screens/cms/MultilingualCmsPage.tsx b/src/frontend/src/screens/cms/MultilingualCmsPage.tsx index bfbc98f..9d0a167 100644 --- a/src/frontend/src/screens/cms/MultilingualCmsPage.tsx +++ b/src/frontend/src/screens/cms/MultilingualCmsPage.tsx @@ -222,7 +222,7 @@ export function MultilingualCmsPage() { ) : contents.length === 0 ? (
콘텐츠가 없습니다.
) : ( -
{t('auction.boq.colSeq')} {t('auction.boq.colTrade')} {t('auction.boq.colItem')}{t('auction.boq.colQty')}{t('auction.boq.colQty')} {t('auction.boq.colUnit')} {t('auction.boq.colBasis')}
{l.item}{l.qty.toLocaleString()}{l.qty.toLocaleString()} {l.unit} {l.basis}
+
diff --git a/src/frontend/src/screens/cms/cms.css b/src/frontend/src/screens/cms/cms.css index a332e77..cca2eef 100644 --- a/src/frontend/src/screens/cms/cms.css +++ b/src/frontend/src/screens/cms/cms.css @@ -39,8 +39,8 @@ display: inline-flex; align-items: center; gap: 4px; - font-size: 11px; - font-weight: 700; + font-size: var(--fs-micro); + font-weight: var(--fw-bold); color: var(--color-warning); background: #fff4e5; padding: 2px 10px; @@ -55,8 +55,8 @@ gap: 4px; padding: 1px 10px; border-radius: var(--radius-pill); - font-size: 11px; - font-weight: 700; + font-size: var(--fs-micro); + font-weight: var(--fw-bold); white-space: nowrap; } .kx-cms-pill--draft { @@ -92,7 +92,7 @@ } .kx-cms-label { font-size: var(--fs-caption); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-500); } .kx-cms-input, @@ -278,7 +278,7 @@ padding: 0 0 8px; font-size: var(--fs-display); line-height: var(--lh-display); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-neutral-900); font-family: inherit; } @@ -331,8 +331,8 @@ var(--color-neutral-050) 16px ); color: var(--color-primary-600); - font-size: 11px; - font-weight: 600; + font-size: var(--fs-micro); + font-weight: var(--fw-semibold); text-align: center; padding: 6px; } @@ -375,7 +375,7 @@ } .kx-cms-block__kind { font-size: var(--fs-caption); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-500); } .kx-cms-block__body { @@ -413,7 +413,7 @@ border-radius: var(--radius-sm); background: var(--color-neutral-050); color: var(--color-primary-700); - font-weight: 600; + font-weight: var(--fw-semibold); font-size: var(--fs-body); flex-wrap: wrap; } @@ -435,8 +435,8 @@ gap: var(--space-2); } .kx-cms-flow__label { - font-size: 11px; - font-weight: 700; + font-size: var(--fs-micro); + font-weight: var(--fw-bold); letter-spacing: 0.04em; text-transform: uppercase; color: var(--color-neutral-500); @@ -474,7 +474,7 @@ } .kx-cms-step.is-current { color: var(--color-primary-700); - font-weight: 700; + font-weight: var(--fw-bold); } .kx-cms-step.is-current .kx-cms-step__dot { border-color: var(--color-primary-600); @@ -502,11 +502,11 @@ } .kx-cms-ver__v { font-size: var(--fs-body); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-neutral-900); } .kx-cms-ver__meta { - font-size: 11px; + font-size: var(--fs-micro); color: var(--color-neutral-500); } .kx-cms-schedcard { @@ -519,8 +519,8 @@ display: flex; align-items: center; gap: 6px; - font-size: 11px; - font-weight: 700; + font-size: var(--fs-micro); + font-weight: var(--fw-bold); color: var(--color-primary-700); margin-bottom: 4px; } @@ -611,7 +611,7 @@ background: transparent; color: var(--color-neutral-500); font-size: var(--fs-body); - font-weight: 600; + font-weight: var(--fw-semibold); cursor: pointer; font-family: inherit; } @@ -680,7 +680,7 @@ display: flex; align-items: center; gap: var(--space-2); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-primary-700); } .kx-mb-site__logo { @@ -692,7 +692,7 @@ display: flex; align-items: center; justify-content: center; - font-weight: 800; + font-weight: var(--fw-bold); font-style: italic; } .kx-mb-site__menu { @@ -718,14 +718,14 @@ border-radius: var(--radius-pill); background: rgba(255, 255, 255, 0.14); border: 1px solid rgba(255, 255, 255, 0.25); - font-size: 11px; - font-weight: 700; + font-size: var(--fs-micro); + font-weight: var(--fw-bold); margin-bottom: var(--space-3); } .kx-mb-hero__title { font-size: 30px; line-height: 1.2; - font-weight: 800; + font-weight: var(--fw-bold); margin-bottom: var(--space-3); max-width: 30ch; } @@ -743,7 +743,7 @@ .kx-mb-btn { padding: 10px 20px; border-radius: var(--radius-sm); - font-weight: 700; + font-weight: var(--fw-bold); font-size: var(--fs-body); cursor: default; border: 1px solid transparent; @@ -777,7 +777,7 @@ } .kx-mb-section__title { font-size: var(--fs-h2); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-neutral-900); } .kx-mb-cards { @@ -808,15 +808,15 @@ border-radius: var(--radius-pill); background: var(--color-primary-600); color: var(--color-on-accent); - font-size: 10px; - font-weight: 700; + font-size: var(--fs-nano); + font-weight: var(--fw-bold); } .kx-mb-card__body { padding: var(--space-3); border-left: 4px solid var(--color-primary-600); } .kx-mb-card__title { - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-neutral-900); margin-bottom: 4px; } @@ -868,8 +868,8 @@ border-radius: var(--radius-pill); background: #e6f4ee; color: var(--color-success); - font-size: 11px; - font-weight: 700; + font-size: var(--fs-micro); + font-weight: var(--fw-bold); } .kx-mb-status__dot { width: 6px; @@ -890,7 +890,7 @@ flex: 1; font-size: var(--fs-caption); color: var(--color-primary-700); - font-weight: 600; + font-weight: var(--fw-semibold); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -917,11 +917,11 @@ } .kx-mb-activity__t { font-size: var(--fs-caption); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-900); } .kx-mb-activity__m { - font-size: 11px; + font-size: var(--fs-micro); color: var(--color-neutral-500); } @@ -951,7 +951,7 @@ align-items: center; justify-content: space-between; font-size: var(--fs-body); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-neutral-900); } .kx-i18n-covcard__pct { @@ -992,45 +992,22 @@ margin-bottom: var(--space-3); } -/* 매트릭스 테이블 */ -.kx-i18n-table { - width: 100%; - border-collapse: collapse; - font-size: var(--fs-body); -} -.kx-i18n-table thead th { - text-align: left; - padding: 10px 12px; - font-size: var(--fs-caption); - font-weight: 600; - letter-spacing: 0.02em; - color: var(--color-neutral-500); - background: var(--color-neutral-050); - border-bottom: 2px solid var(--color-neutral-200); - white-space: nowrap; - vertical-align: bottom; -} +/* 매트릭스 테이블 — 기본 그리드는 정본 .kx-table 로 수렴. 아래는 확장만 유지. */ .kx-i18n-table tbody td { - padding: 10px 12px; - border-bottom: 1px solid var(--color-neutral-200); - color: var(--color-neutral-700); vertical-align: top; } .kx-i18n-table tbody tr { cursor: pointer; } -.kx-i18n-table tbody tr:hover { - background: var(--color-neutral-050); -} .kx-i18n-table tbody tr.is-active { background: var(--color-primary-050); } .kx-i18n-row__key { - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-neutral-900); } .kx-i18n-row__id { - font-size: 11px; + font-size: var(--fs-micro); color: var(--color-neutral-500); font-family: var(--font-mono); } @@ -1100,13 +1077,13 @@ background: var(--color-ai-surface); } .kx-i18n-tone__t { - font-size: 11px; - font-weight: 700; + font-size: var(--fs-micro); + font-weight: var(--fw-bold); color: var(--color-ai-accent); margin-bottom: 4px; } .kx-i18n-tone__p { - font-size: 11px; + font-size: var(--fs-micro); line-height: 1.5; color: var(--color-neutral-700); font-style: italic; @@ -1119,8 +1096,8 @@ /* 섹션 라벨(공통 소제목) */ .kx-cms-sublabel { - font-size: 11px; - font-weight: 700; + font-size: var(--fs-micro); + font-weight: var(--fw-bold); letter-spacing: 0.04em; text-transform: uppercase; color: var(--color-neutral-500); @@ -1162,11 +1139,11 @@ background: color-mix(in srgb, var(--color-success) 10%, transparent); color: var(--color-success); font-size: 13px; - font-weight: 600; + font-weight: var(--fw-semibold); } .kx-cms-disabled-hint { margin-left: auto; - font-size: 11px; + font-size: var(--fs-micro); color: var(--color-neutral-500); border: 1px dashed var(--color-neutral-300); border-radius: 6px; diff --git a/src/frontend/src/screens/compliance/compliance-report.css b/src/frontend/src/screens/compliance/compliance-report.css index 446358e..af69179 100644 --- a/src/frontend/src/screens/compliance/compliance-report.css +++ b/src/frontend/src/screens/compliance/compliance-report.css @@ -17,7 +17,7 @@ padding: var(--space-3) var(--space-4); border-radius: var(--radius-lg); font-size: var(--fs-body); - font-weight: 600; + font-weight: var(--fw-semibold); } .kx-comp__banner.is-block { background: #fef3f2; @@ -30,7 +30,7 @@ border: 1px solid #a6e0c8; } .kx-comp__banner-note { - font-weight: 400; + font-weight: var(--fw-regular); } .kx-comp__drop { @@ -81,8 +81,8 @@ gap: var(--space-2); } .kx-comp__lg { - font-size: 11px; - font-weight: 600; + font-size: var(--fs-micro); + font-weight: var(--fw-semibold); padding: 1px 8px; border-radius: var(--radius-pill); } @@ -130,8 +130,8 @@ display: flex; align-items: center; justify-content: center; - font-size: 12px; - font-weight: 700; + font-size: var(--fs-caption); + font-weight: var(--fw-bold); flex-shrink: 0; background: var(--color-error); color: var(--color-on-accent); @@ -156,12 +156,12 @@ } .kx-comp__item-label { font-size: var(--fs-body); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-900); } .kx-comp__sev { - font-size: 11px; - font-weight: 700; + font-size: var(--fs-micro); + font-weight: var(--fw-bold); padding: 1px 8px; border-radius: var(--radius-pill); } @@ -182,8 +182,8 @@ color: var(--color-neutral-500); } .kx-comp__doc { - font-size: 11px; - font-weight: 600; + font-size: var(--fs-micro); + font-weight: var(--fw-semibold); color: var(--color-ai-accent); background: var(--color-ai-surface); border: 1px solid var(--color-ai-accent); @@ -231,8 +231,8 @@ position: absolute; top: 12%; right: 6%; - font-size: 11px; - font-weight: 700; + font-size: var(--fs-micro); + font-weight: var(--fw-bold); color: var(--color-on-accent); background: var(--color-error); padding: 3px 8px; diff --git a/src/frontend/src/screens/contractor/contractor.css b/src/frontend/src/screens/contractor/contractor.css index 04ae70f..500ba68 100644 --- a/src/frontend/src/screens/contractor/contractor.css +++ b/src/frontend/src/screens/contractor/contractor.css @@ -14,7 +14,7 @@ font-size: var(--fs-h1); line-height: var(--lh-h1); color: var(--color-neutral-900); - font-weight: 700; + font-weight: var(--fw-bold); } .kx-cbd__subtitle { font-size: var(--fs-body); @@ -78,7 +78,7 @@ padding: 2px 10px; border-radius: var(--radius-sm); font-size: var(--fs-caption); - font-weight: 600; + font-weight: var(--fw-semibold); background: var(--color-primary-100); color: var(--color-primary-700); } @@ -88,7 +88,7 @@ } .kx-booth__name { font-size: var(--fs-h3); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-neutral-900); } .kx-booth__meta { @@ -112,7 +112,7 @@ } .kx-booth__right-value { font-size: var(--fs-h3); - font-weight: 700; + font-weight: var(--fw-bold); } /* 시공 스테퍼 */ @@ -148,7 +148,7 @@ align-items: center; justify-content: center; font-size: var(--fs-caption); - font-weight: 700; + font-weight: var(--fw-bold); background: var(--color-neutral-100); color: var(--color-neutral-500); } @@ -168,7 +168,7 @@ .kx-step--done .kx-step__label, .kx-step--current .kx-step__label { color: var(--color-primary-700); - font-weight: 600; + font-weight: var(--fw-semibold); } .kx-booth__actions { @@ -220,7 +220,7 @@ } .kx-cbd-bid__name { font-size: var(--fs-body); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-900); } .kx-cbd-bid__price { @@ -236,8 +236,8 @@ gap: 4px; } .kx-rankpill { - font-size: 11px; - font-weight: 700; + font-size: var(--fs-micro); + font-weight: var(--fw-bold); padding: 1px 8px; border-radius: var(--radius-pill); } diff --git a/src/frontend/src/screens/dashboard/dashboard.css b/src/frontend/src/screens/dashboard/dashboard.css index 4658453..798a2a1 100644 --- a/src/frontend/src/screens/dashboard/dashboard.css +++ b/src/frontend/src/screens/dashboard/dashboard.css @@ -29,7 +29,7 @@ background: var(--color-primary-600); color: var(--color-on-accent); font-size: var(--fs-h2); - font-weight: 700; + font-weight: var(--fw-bold); } /* 히어로 타임라인 */ @@ -78,7 +78,7 @@ align-items: center; justify-content: center; font-size: 13px; - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-on-accent); background: var(--color-neutral-200); } @@ -113,7 +113,7 @@ } .kx-timeline__title { font-size: var(--fs-body); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-900); } .kx-timeline__card { @@ -122,7 +122,7 @@ } .kx-timeline__imminent { color: var(--color-violation-warn-text); - font-weight: 700; + font-weight: var(--fw-bold); } .kx-timeline__muted { color: var(--color-neutral-500); @@ -150,7 +150,7 @@ .kx-kpi__value { font-size: var(--fs-display); line-height: var(--lh-display); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-neutral-900); } .kx-kpi__sub { @@ -162,7 +162,7 @@ } .kx-kpi--warn .kx-kpi__sub { color: var(--color-violation-warn-text); - font-weight: 600; + font-weight: var(--fw-semibold); } .kx-kpi--error .kx-kpi__value { color: var(--color-error); @@ -193,7 +193,7 @@ color: var(--color-neutral-900); } .kx-dash__degraded { - font-size: 11px; + font-size: var(--fs-micro); color: var(--color-warning); background: #fff4e5; padding: 2px 8px; @@ -222,7 +222,7 @@ text-align: left; padding: 10px 12px; font-size: var(--fs-caption); - font-weight: 600; + font-weight: var(--fw-semibold); letter-spacing: 0.02em; color: var(--color-neutral-500); background: var(--color-neutral-050); diff --git a/src/frontend/src/screens/design/studio.css b/src/frontend/src/screens/design/studio.css index 3ce3889..9199f4c 100644 --- a/src/frontend/src/screens/design/studio.css +++ b/src/frontend/src/screens/design/studio.css @@ -22,7 +22,7 @@ } .kx-studio__ctx h2 { font-size: var(--fs-h3); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-neutral-900); } .kx-studio__ctx-sub { @@ -36,7 +36,7 @@ background: var(--color-white); border-radius: var(--radius-sm); color: var(--color-neutral-700); - font-size: 16px; + font-size: var(--fs-h3); cursor: pointer; } .kx-studio__back:hover { @@ -64,7 +64,7 @@ justify-content: center; background: var(--color-ai-surface); color: var(--color-ai-accent); - font-size: 20px; + font-size: var(--fs-h2); } .kx-studio__body { @@ -100,7 +100,7 @@ border: none; background: transparent; font-size: var(--fs-h3); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-900); } .kx-acc__chevron { @@ -159,7 +159,7 @@ color: var(--color-neutral-700); } .kx-studio__toggle--req { - font-weight: 600; + font-weight: var(--fw-semibold); } .kx-studio__seg { display: inline-flex; @@ -173,7 +173,7 @@ background: transparent; padding: 6px 18px; border-radius: var(--radius-sm); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-500); } .kx-studio__seg button.is-active { @@ -208,7 +208,7 @@ color: var(--color-neutral-500); } .kx-studio__tile-label { - font-weight: 600; + font-weight: var(--fw-semibold); } .kx-studio__tile-empty button { border: 1px solid var(--color-ai-accent); @@ -217,7 +217,7 @@ border-radius: var(--radius-sm); padding: 6px 14px; font-size: var(--fs-caption); - font-weight: 600; + font-weight: var(--fw-semibold); } /* Before/After */ @@ -244,7 +244,7 @@ transform: rotate(-24deg); color: rgba(255, 255, 255, 0.55); font-size: 13px; - font-weight: 700; + font-weight: var(--fw-bold); letter-spacing: 0.04em; pointer-events: none; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.25); @@ -255,7 +255,7 @@ left: var(--space-2); } .kx-studio__compare-notice { - font-size: 11px; + font-size: var(--fs-micro); line-height: 15px; color: var(--color-neutral-500); } @@ -287,7 +287,7 @@ padding: 2px 10px; border-radius: var(--radius-pill); font-size: var(--fs-caption); - font-weight: 600; + font-weight: var(--fw-semibold); } .kx-chip--pass { background: #e6f4ee; diff --git a/src/frontend/src/screens/docs/docs.css b/src/frontend/src/screens/docs/docs.css index 9b4d072..0eafd58 100644 --- a/src/frontend/src/screens/docs/docs.css +++ b/src/frontend/src/screens/docs/docs.css @@ -42,7 +42,7 @@ } .kx-doc__label { font-size: var(--fs-caption); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-500); } .kx-doc__input, @@ -153,14 +153,14 @@ } .kx-doc__step-label { font-size: var(--fs-caption); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-neutral-700); } .kx-doc__step.is-active .kx-doc__step-label { color: var(--color-warning); } .kx-doc__step-sub { - font-size: 11px; + font-size: var(--fs-micro); color: var(--color-neutral-500); } @@ -207,7 +207,7 @@ } .kx-doc__progress-num { font-size: var(--fs-caption); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-primary-700); } .kx-doc__progress-track { @@ -254,7 +254,7 @@ color: var(--color-ai-accent); } .kx-doc__doc-name { - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-900); font-size: var(--fs-body); } @@ -269,7 +269,7 @@ padding: 1px 10px; border-radius: var(--radius-pill); font-size: var(--fs-caption); - font-weight: 600; + font-weight: var(--fw-semibold); background: var(--color-neutral-100); color: var(--color-neutral-500); white-space: nowrap; @@ -321,7 +321,7 @@ } .kx-doc__issue-title { font-size: var(--fs-caption); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-neutral-900); } .kx-doc__issue-desc { @@ -340,7 +340,7 @@ align-items: center; gap: 6px; font-size: var(--fs-caption); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-ai-accent); margin-bottom: 4px; } @@ -366,7 +366,7 @@ .kx-doc__stat-value { font-size: var(--fs-display); line-height: 1; - font-weight: 700; + font-weight: var(--fw-bold); margin: var(--space-2) 0; font-variant-numeric: tabular-nums; } @@ -474,7 +474,7 @@ .kx-rep__a4-title h2 { display: inline-block; font-size: 22px; - font-weight: 700; + font-weight: var(--fw-bold); letter-spacing: 0.3em; border-bottom: 3px solid #141b2c; padding: 0 16px 6px; @@ -489,19 +489,19 @@ .kx-rep__table td { border: 1px solid #141b2c; padding: 7px 9px; - font-size: 12px; + font-size: var(--fs-caption); text-align: left; vertical-align: top; } .kx-rep__table th { background: #f2f4f7; - font-weight: 700; + font-weight: var(--fw-bold); width: 22%; white-space: nowrap; } .kx-rep__a4 h4 { - font-size: 14px; - font-weight: 700; + font-size: var(--fs-body); + font-weight: var(--fw-bold); margin: 0 0 10px; } .kx-rep__a4 section { @@ -511,7 +511,7 @@ border: 1px solid #141b2c; padding: 12px; min-height: 90px; - font-size: 12px; + font-size: var(--fs-caption); } .kx-rep__a4 ul { margin: 0; @@ -534,8 +534,8 @@ gap: 14px; } .kx-rep__sign-name { - font-size: 14px; - font-weight: 700; + font-size: var(--fs-body); + font-weight: var(--fw-bold); } .kx-rep__stamp { width: 54px; @@ -543,8 +543,8 @@ border: 2px solid var(--color-error); border-radius: 50%; color: var(--color-error); - font-size: 10px; - font-weight: 700; + font-size: var(--fs-nano); + font-weight: var(--fw-bold); display: flex; align-items: center; justify-content: center; diff --git a/src/frontend/src/screens/exhibitor/exhibitor-home.css b/src/frontend/src/screens/exhibitor/exhibitor-home.css index 3a065da..56f8189 100644 --- a/src/frontend/src/screens/exhibitor/exhibitor-home.css +++ b/src/frontend/src/screens/exhibitor/exhibitor-home.css @@ -20,7 +20,7 @@ .kx-exh__company { font-size: var(--fs-display); line-height: var(--lh-display); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-neutral-900); margin-bottom: var(--space-2); } @@ -101,7 +101,7 @@ display: flex; align-items: center; justify-content: center; - font-weight: 700; + font-weight: var(--fw-bold); border: 2px solid var(--color-neutral-200); color: var(--color-neutral-500); background: var(--color-white); @@ -125,7 +125,7 @@ } .kx-stepper__label { font-size: var(--fs-caption); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-700); } .kx-stepper__item.is-current .kx-stepper__label { @@ -135,7 +135,7 @@ position: absolute; top: 62px; white-space: nowrap; - font-size: 11px; + font-size: var(--fs-micro); color: var(--color-primary-700); background: var(--color-primary-050); padding: 2px 8px; @@ -206,7 +206,7 @@ background: none; color: var(--color-primary-600); font-size: var(--fs-body); - font-weight: 600; + font-weight: var(--fw-semibold); cursor: pointer; padding: 0; } @@ -277,7 +277,7 @@ left: 0; right: 0; padding: var(--space-3); - font-size: 11px; + font-size: var(--fs-micro); color: rgba(255, 255, 255, 0.85); background: linear-gradient(to top, rgba(0, 0, 0, 0.7), transparent); } @@ -290,7 +290,7 @@ transform: rotate(-24deg); color: rgba(255, 255, 255, 0.5); font-size: 15px; - font-weight: 700; + font-weight: var(--fw-bold); letter-spacing: 0.04em; pointer-events: none; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.25); diff --git a/src/frontend/src/screens/floorplan/auto-layout.css b/src/frontend/src/screens/floorplan/auto-layout.css index c461530..b23d1ad 100644 --- a/src/frontend/src/screens/floorplan/auto-layout.css +++ b/src/frontend/src/screens/floorplan/auto-layout.css @@ -25,7 +25,7 @@ align-items: center; gap: var(--space-2); font-size: var(--fs-h2); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-900); } .kx-auto__sub { @@ -36,7 +36,7 @@ .kx-auto__close { background: none; border: none; - font-size: 16px; + font-size: var(--fs-h3); color: var(--color-neutral-500); padding: 4px; } @@ -96,7 +96,7 @@ } .kx-auto__card-title { font-size: var(--fs-h3); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-900); } .kx-auto__metrics { @@ -111,9 +111,9 @@ } .kx-auto__viol-ok { color: var(--color-success); - font-weight: 600; + font-weight: var(--fw-semibold); } .kx-auto__viol-bad { color: var(--color-error); - font-weight: 600; + font-weight: var(--fw-semibold); } diff --git a/src/frontend/src/screens/floorplan/editor.css b/src/frontend/src/screens/floorplan/editor.css index e95e703..c6d89f8 100644 --- a/src/frontend/src/screens/floorplan/editor.css +++ b/src/frontend/src/screens/floorplan/editor.css @@ -24,7 +24,7 @@ } .kx-editor__hall { font-size: var(--fs-h3); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-neutral-900); } .kx-editor__version { @@ -40,8 +40,8 @@ font-size: 8px; } .kx-editor__degraded { - font-size: 11px; - font-weight: 600; + font-size: var(--fs-micro); + font-weight: var(--fw-semibold); color: var(--color-warning); background: #fff4e5; padding: 2px 8px; @@ -81,8 +81,8 @@ border-bottom: var(--border-card); } .kx-tool-group h3 { - font-size: 11px; - font-weight: 700; + font-size: var(--fs-micro); + font-weight: var(--fw-bold); letter-spacing: 0.04em; text-transform: uppercase; color: var(--color-neutral-500); @@ -123,7 +123,7 @@ height: 28px; } .kx-palette__label { - font-size: 11px; + font-size: var(--fs-micro); text-align: center; color: var(--color-neutral-700); } @@ -247,7 +247,7 @@ .kx-editor__zoom span { width: 44px; text-align: center; - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-700); } @@ -264,7 +264,7 @@ border: none; border-bottom: 2px solid transparent; font-size: var(--fs-body); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-500); } .kx-editor__tabs button.is-active { @@ -298,7 +298,7 @@ margin: 0; color: var(--color-neutral-900); font-size: var(--fs-body); - font-weight: 500; + font-weight: var(--fw-medium); } /* 검증 */ @@ -319,7 +319,7 @@ padding: 4px 12px; border-radius: var(--radius-pill); font-size: var(--fs-caption); - font-weight: 700; + font-weight: var(--fw-bold); } .kx-valid__chip--block { background: #fef3f2; @@ -332,7 +332,7 @@ .kx-valid__ok { background: #e6f4ee; color: var(--color-success); - font-weight: 600; + font-weight: var(--fw-semibold); padding: var(--space-3); border-radius: var(--radius-sm); font-size: var(--fs-body); @@ -364,8 +364,8 @@ height: 18px; border-radius: 50%; color: var(--color-on-accent); - font-size: 11px; - font-weight: 700; + font-size: var(--fs-micro); + font-weight: var(--fw-bold); display: flex; align-items: center; justify-content: center; @@ -389,15 +389,15 @@ } .kx-valid__label { font-size: var(--fs-body); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-900); } .kx-valid__locate { background: none; border: none; color: var(--color-primary-600); - font-size: 11px; - font-weight: 600; + font-size: var(--fs-micro); + font-weight: var(--fw-semibold); flex-shrink: 0; } .kx-valid__measured { @@ -408,8 +408,8 @@ .kx-valid__doc { display: inline-block; margin-top: var(--space-2); - font-size: 11px; - font-weight: 600; + font-size: var(--fs-micro); + font-weight: var(--fw-semibold); color: var(--color-ai-accent); background: var(--color-ai-surface); border: 1px solid var(--color-ai-accent); @@ -417,7 +417,7 @@ padding: 2px 8px; } .kx-valid__disclaimer { - font-size: 11px; + font-size: var(--fs-micro); line-height: 15px; color: var(--color-neutral-500); } diff --git a/src/frontend/src/screens/floorplan/layout-comparison.css b/src/frontend/src/screens/floorplan/layout-comparison.css index c781ceb..0317bfa 100644 --- a/src/frontend/src/screens/floorplan/layout-comparison.css +++ b/src/frontend/src/screens/floorplan/layout-comparison.css @@ -75,8 +75,8 @@ display: flex; align-items: center; justify-content: center; - font-size: 14px; - font-weight: 700; + font-size: var(--fs-body); + font-weight: var(--fw-bold); } .kx-cmp__wm { position: absolute; @@ -87,7 +87,7 @@ transform: rotate(-24deg); color: rgba(255, 255, 255, 0.5); font-size: 13px; - font-weight: 700; + font-weight: var(--fw-bold); letter-spacing: 0.04em; pointer-events: none; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); @@ -107,8 +107,8 @@ color: var(--color-neutral-900); } .kx-cmp__reco { - font-size: 11px; - font-weight: 700; + font-size: var(--fs-micro); + font-weight: var(--fw-bold); color: var(--color-primary-700); background: var(--color-primary-100); padding: 2px 8px; @@ -130,7 +130,7 @@ } .kx-cmp__metrics dd { color: var(--color-neutral-900); - font-weight: 600; + font-weight: var(--fw-semibold); } .kx-cmp__ok { color: var(--color-success); diff --git a/src/frontend/src/screens/gallery/gallery.css b/src/frontend/src/screens/gallery/gallery.css index 613f037..ac2651d 100644 --- a/src/frontend/src/screens/gallery/gallery.css +++ b/src/frontend/src/screens/gallery/gallery.css @@ -16,7 +16,7 @@ border: 1px solid var(--color-ai-accent); color: var(--color-ai-accent); font-size: var(--fs-caption); - font-weight: 600; + font-weight: var(--fw-semibold); } .kx-gallery__head { @@ -58,7 +58,7 @@ padding: 5px 12px; border-radius: var(--radius-pill); font-size: var(--fs-caption); - font-weight: 600; + font-weight: var(--fw-semibold); } .kx-gallery__chip.is-active { background: var(--color-primary-600); @@ -90,7 +90,7 @@ color: var(--color-neutral-500); } .kx-gallery__status { - font-weight: 600; + font-weight: var(--fw-semibold); padding: 1px 8px; border-radius: var(--radius-pill); } @@ -151,7 +151,7 @@ } .kx-lightbox__notice { margin-top: var(--space-3); - font-size: 11px; + font-size: var(--fs-micro); color: var(--color-neutral-500); } diff --git a/src/frontend/src/screens/hallassign/hallassign.css b/src/frontend/src/screens/hallassign/hallassign.css index 58ca34e..59f0a68 100644 --- a/src/frontend/src/screens/hallassign/hallassign.css +++ b/src/frontend/src/screens/hallassign/hallassign.css @@ -57,7 +57,7 @@ gap: 2px; } .kx-hall__hallname { - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-900); font-size: var(--fs-body); } @@ -114,7 +114,7 @@ } .kx-hall__chiptext em { font-style: normal; - font-size: 11px; + font-size: var(--fs-micro); color: var(--color-neutral-500); } .kx-hall__chip.is-conflict .kx-hall__chiptext em { @@ -124,7 +124,7 @@ border: none; background: transparent; cursor: pointer; - font-size: 16px; + font-size: var(--fs-h3); line-height: 1; color: var(--color-neutral-400); padding: 0 2px; @@ -188,7 +188,7 @@ color: var(--color-success); } .kx-hall__total td { - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-neutral-900); border-top: 2px solid var(--color-neutral-200); } @@ -316,7 +316,7 @@ } .kx-hall__detailsec h3 { font-size: var(--fs-body); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-900); margin: 0; } diff --git a/src/frontend/src/screens/home/home.css b/src/frontend/src/screens/home/home.css index 9e6f169..c49bd46 100644 --- a/src/frontend/src/screens/home/home.css +++ b/src/frontend/src/screens/home/home.css @@ -24,7 +24,7 @@ } .kx-home__section-title { font-size: var(--fs-h3); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-neutral-900); margin: 0; } @@ -53,7 +53,7 @@ } .kx-home__hello { font-size: var(--fs-h1); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-neutral-900); margin: 0 0 4px; } @@ -108,7 +108,7 @@ } .kx-kpitile__value { font-size: var(--fs-h2); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-neutral-900); } /* Stitch 정합: 임박/경과 타일 값 강조색 */ @@ -171,7 +171,7 @@ } .kx-home__banner-title { font-size: var(--fs-h1); - font-weight: 700; + font-weight: var(--fw-bold); margin: 0 0 6px; text-shadow: 0 1px 4px rgba(0, 0, 0, 0.3); } @@ -295,7 +295,7 @@ .kx-poster__fallback-text { color: #fff; font-size: var(--fs-h3); - font-weight: 700; + font-weight: var(--fw-bold); text-align: center; line-height: 1.35; } @@ -316,8 +316,8 @@ border-left-color: var(--color-neutral-500); } .kx-poster__hall { - font-size: 10px; - font-weight: 700; + font-size: var(--fs-nano); + font-weight: var(--fw-bold); color: var(--color-primary-600); margin-bottom: 1px; } @@ -329,7 +329,7 @@ } .kx-poster__name { font-size: var(--fs-body); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-900); display: -webkit-box; -webkit-line-clamp: 2; @@ -354,7 +354,7 @@ padding: 2px 8px; border-radius: var(--radius-pill); font-size: var(--fs-caption); - font-weight: 600; + font-weight: var(--fw-semibold); } .kx-home__chip.is-ongoing { background: var(--color-success-bg); @@ -405,7 +405,7 @@ } .kx-home__cal-title { font-size: var(--fs-h3); - font-weight: 700; + font-weight: var(--fw-bold); min-width: 120px; text-align: center; } @@ -470,7 +470,7 @@ padding: 6px 0; text-align: center; font-size: var(--fs-caption); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-500); } .kx-home__cal-wd.is-sun { @@ -528,11 +528,11 @@ color: #fff; border-radius: var(--radius-pill); padding: 1px 7px; - font-weight: 700; + font-weight: var(--fw-bold); } .kx-home__cal-more { margin-top: auto; - font-size: 10px; + font-size: var(--fs-nano); color: var(--color-neutral-500); } /* spanning bar 레인 — 셀 위에 절대배치(요일 헤더 아래 24px 오프셋) */ @@ -548,7 +548,7 @@ height: 18px; line-height: 18px; border-radius: 3px; - font-size: 11px; + font-size: var(--fs-micro); padding: 0 6px; color: #fff; white-space: nowrap; @@ -578,7 +578,7 @@ } .kx-home__cal-list-title { font-size: var(--fs-body); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-neutral-900); margin: 0 0 var(--space-2); } @@ -609,7 +609,7 @@ .kx-home__cal-list-name { font-size: var(--fs-body); color: var(--color-neutral-900); - font-weight: 500; + font-weight: var(--fw-medium); flex: 1; min-width: 0; overflow: hidden; @@ -701,7 +701,7 @@ } .kx-home__widget-head h2 { font-size: var(--fs-h3); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-neutral-900); margin: 0; } @@ -713,7 +713,7 @@ height: 20px; padding: 0 6px; font-size: var(--fs-caption); - font-weight: 700; + font-weight: var(--fw-bold); display: inline-flex; align-items: center; justify-content: center; @@ -767,7 +767,7 @@ .kx-home__todo-title { font-size: var(--fs-body); color: var(--color-neutral-900); - font-weight: 500; + font-weight: var(--fw-medium); } .kx-home__todo-target { font-size: var(--fs-caption); @@ -778,7 +778,7 @@ } .kx-home__todo-due { font-size: var(--fs-caption); - font-weight: 700; + font-weight: var(--fw-bold); flex-shrink: 0; } .kx-home__todo-due.is-normal { @@ -802,7 +802,7 @@ color: var(--color-success); } .kx-home__todo-done-title { - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-neutral-900); margin: 0; } @@ -850,8 +850,8 @@ .kx-home__new { background: var(--color-error); color: #fff; - font-size: 10px; - font-weight: 700; + font-size: var(--fs-nano); + font-weight: var(--fw-bold); border-radius: var(--radius-sm); padding: 1px 5px; } @@ -951,7 +951,7 @@ } .kx-home__ws-card-title { font-size: var(--fs-h3); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-neutral-900); margin: 4px 0 0; } @@ -969,7 +969,7 @@ flex-wrap: wrap; } .kx-home__ws-empty-title { - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-neutral-900); margin: 0 0 4px; } @@ -1051,7 +1051,7 @@ } .kx-home__chart-cap { font-size: var(--fs-caption); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-neutral-700); margin-bottom: var(--space-2); } @@ -1084,7 +1084,7 @@ } .kx-home__chart-legend-item b { color: var(--color-neutral-900); - font-weight: 700; + font-weight: var(--fw-bold); } @media (max-width: 1100px) { @@ -1145,7 +1145,7 @@ gap: var(--space-2); margin: 0; font-size: var(--fs-h3); - font-weight: 800; + font-weight: var(--fw-bold); color: var(--color-ai-accent-strong); letter-spacing: -0.01em; } @@ -1227,14 +1227,14 @@ align-items: center; gap: var(--space-1); font-size: var(--fs-caption); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-neutral-900); line-height: 1.25; } .kx-home__aihub-card-tag { flex: 0 0 auto; - font-size: 10px; - font-weight: 800; + font-size: var(--fs-nano); + font-weight: var(--fw-bold); letter-spacing: 0.04em; padding: 1px 5px; border-radius: var(--radius-pill); diff --git a/src/frontend/src/screens/login/login.css b/src/frontend/src/screens/login/login.css index 6d83216..3018fce 100644 --- a/src/frontend/src/screens/login/login.css +++ b/src/frontend/src/screens/login/login.css @@ -99,14 +99,14 @@ background: rgba(255, 255, 255, 0.1); color: #d3c7ff; font-size: var(--fs-caption); - font-weight: 600; + font-weight: var(--fw-semibold); backdrop-filter: blur(4px); } .kx-login__headline { color: var(--color-on-accent); font-size: var(--fs-display); line-height: var(--lh-display); - font-weight: 700; + font-weight: var(--fw-bold); letter-spacing: -0.01em; } @media (min-width: 1280px) { @@ -153,12 +153,12 @@ display: flex; align-items: center; justify-content: center; - font-weight: 700; + font-weight: var(--fw-bold); font-size: 18px; } .kx-login__logo-text { font-size: var(--fs-h2); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-primary-700); } .kx-login__logo-img { @@ -168,7 +168,7 @@ } .kx-login__logo-suffix { font-size: var(--fs-body); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-500); padding-left: 2px; border-left: 1px solid var(--color-neutral-200); @@ -185,7 +185,7 @@ .kx-login__title { font-size: var(--fs-h1); line-height: var(--lh-h1); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-neutral-900); text-align: center; } @@ -224,7 +224,7 @@ border: none; color: var(--color-primary-600); font-size: var(--fs-body); - font-weight: 500; + font-weight: var(--fw-medium); text-align: center; } .kx-login__invite-link:hover { @@ -259,7 +259,7 @@ padding: 0; color: var(--color-primary-600); font-size: var(--fs-caption); - font-weight: 600; + font-weight: var(--fw-semibold); cursor: pointer; } .kx-login__text-link:hover { @@ -308,7 +308,7 @@ background: var(--color-success, #16a34a); } .kx-pw__label { - font-size: 11px; + font-size: var(--fs-micro); color: var(--color-neutral-500); } .kx-pw__label--error { @@ -335,7 +335,7 @@ position: absolute; bottom: var(--space-5); color: var(--color-neutral-500); - font-size: 11px; + font-size: var(--fs-micro); } /* ── 공통 필드 ── */ @@ -346,7 +346,7 @@ } .kx-field label { font-size: var(--fs-caption); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-700); } .kx-field input { @@ -383,7 +383,7 @@ .kx-ws__header h1 { font-size: var(--fs-h1); line-height: var(--lh-h1); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-neutral-900); } .kx-ws__header p { @@ -419,7 +419,7 @@ .kx-ws__card-title { font-size: var(--fs-h3); line-height: var(--lh-h3); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-900); } .kx-ws__card-meta { @@ -464,7 +464,7 @@ .kx-modal__title { font-size: var(--fs-h2); line-height: var(--lh-h2); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-900); margin-bottom: var(--space-2); } @@ -494,7 +494,7 @@ } .kx-modal__blocked h3 { font-size: var(--fs-h2); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-error); } .kx-modal__blocked p { @@ -567,7 +567,7 @@ .kx-otp__code-input { letter-spacing: 6px; text-align: center; - font-size: 20px; + font-size: var(--fs-h2); } .kx-otp__ok { font-size: var(--fs-body); diff --git a/src/frontend/src/screens/marketing/marketing.css b/src/frontend/src/screens/marketing/marketing.css index 86d43a1..0c30aeb 100644 --- a/src/frontend/src/screens/marketing/marketing.css +++ b/src/frontend/src/screens/marketing/marketing.css @@ -38,8 +38,8 @@ /* 샘플 데이터 배지 */ .kx-sample { - font-size: 11px; - font-weight: 700; + font-size: var(--fs-micro); + font-weight: var(--fw-bold); color: var(--color-warning); background: #fff4e5; border: 1px solid #fcd9a8; @@ -50,8 +50,8 @@ /* 오프라인 강등(폴백) 표시 — 집계 API 미연결 시 시연 데이터 안내 */ .kx-vis__degraded { - font-size: 11px; - font-weight: 700; + font-size: var(--fs-micro); + font-weight: var(--fw-bold); color: var(--color-warning); background: #fff4e5; border: 1px solid #fcd9a8; @@ -72,7 +72,7 @@ align-items: center; gap: 4px; font-size: var(--fs-caption); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-700); background: var(--color-neutral-100); border: 1px solid transparent; @@ -93,7 +93,7 @@ } .kx-kpi__unit { font-size: var(--fs-h3); - font-weight: 600; + font-weight: var(--fw-semibold); margin-left: 3px; color: var(--color-neutral-500); } @@ -140,7 +140,7 @@ border: none; background: none; font-size: var(--fs-caption); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-primary-600); cursor: pointer; } @@ -207,7 +207,7 @@ } .kx-mkt__campaign-top h3 { font-size: var(--fs-body); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-neutral-900); overflow: hidden; text-overflow: ellipsis; @@ -248,8 +248,8 @@ /* 캠페인 상태 pill */ .kx-mkt__cpill { flex-shrink: 0; - font-size: 11px; - font-weight: 700; + font-size: var(--fs-micro); + font-weight: var(--fw-bold); padding: 2px 8px; border-radius: 4px; } @@ -277,7 +277,7 @@ } .kx-mkt__field-label { font-size: var(--fs-caption); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-900); margin: var(--space-4) 0 var(--space-2); } @@ -323,7 +323,7 @@ align-items: center; justify-content: space-between; font-size: var(--fs-caption); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-ai-accent); margin-bottom: var(--space-2); } @@ -385,8 +385,8 @@ .kx-mkt__preview-head { padding: var(--space-2); text-align: center; - font-size: 11px; - font-weight: 700; + font-size: var(--fs-micro); + font-weight: var(--fw-bold); color: var(--color-neutral-500); background: var(--color-neutral-050); border-bottom: 1px solid var(--color-neutral-200); @@ -409,7 +409,7 @@ } .kx-mkt__preview-title { font-size: var(--fs-body); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-neutral-900); margin-bottom: var(--space-2); } @@ -422,7 +422,7 @@ margin-top: var(--space-4); padding-top: var(--space-3); border-top: 1px solid var(--color-neutral-100); - font-size: 10px; + font-size: var(--fs-nano); color: var(--color-neutral-500); } .kx-mkt__preview-links { @@ -463,8 +463,8 @@ margin-bottom: var(--space-3); } .kx-mkt__tier-badge { - font-size: 11px; - font-weight: 700; + font-size: var(--fs-micro); + font-weight: var(--fw-bold); letter-spacing: 0.08em; color: var(--tier-accent, var(--color-primary-700)); background: color-mix(in srgb, var(--tier-accent, #0066b3) 12%, white); @@ -478,7 +478,7 @@ } .kx-mkt__tier-price { font-size: var(--fs-h2); - font-weight: 800; + font-weight: var(--fw-bold); color: var(--color-primary-700); margin-bottom: var(--space-4); } @@ -511,7 +511,7 @@ } .kx-mkt__tier-remain { font-size: var(--fs-caption); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-700); } .kx-mkt__tier-remain strong { @@ -598,7 +598,7 @@ border-radius: var(--radius-sm); background: var(--color-primary-100); color: var(--color-primary-700); - font-weight: 700; + font-weight: var(--fw-bold); display: inline-flex; align-items: center; justify-content: center; @@ -619,8 +619,8 @@ } .kx-mkt__spill { flex-shrink: 0; - font-size: 11px; - font-weight: 700; + font-size: var(--fs-micro); + font-weight: var(--fw-bold); padding: 2px 8px; border-radius: 4px; } @@ -678,7 +678,7 @@ background: var(--color-primary-050); color: var(--color-primary-700); font-size: var(--fs-body); - font-weight: 700; + font-weight: var(--fw-bold); cursor: pointer; } .kx-mkt__roi-link:hover { @@ -714,5 +714,5 @@ background: var(--kx-success-bg, #ecfdf5); color: var(--kx-success-fg, #065f46); border: 1px solid var(--kx-success-border, #a7f3d0); - font-size: 14px; + font-size: var(--fs-body); } diff --git a/src/frontend/src/screens/mobile/mobile.css b/src/frontend/src/screens/mobile/mobile.css index 6253371..0115100 100644 --- a/src/frontend/src/screens/mobile/mobile.css +++ b/src/frontend/src/screens/mobile/mobile.css @@ -66,10 +66,10 @@ } .kx-m__accent { color: var(--color-primary-600); - font-weight: 700; + font-weight: var(--fw-bold); } .kx-m__offline { - font-size: 11px; + font-size: var(--fs-micro); color: var(--color-neutral-500); background: var(--color-neutral-100); border: 1px solid var(--color-neutral-200); @@ -99,7 +99,7 @@ padding: 7px; border-radius: var(--radius-sm); font-size: var(--fs-caption); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-500); cursor: pointer; } @@ -125,7 +125,7 @@ border-radius: var(--radius-lg); padding: var(--space-3); font-size: var(--fs-body); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-primary-700); } @@ -149,7 +149,7 @@ } .kx-m__acc-head span:first-child { font-size: var(--fs-h3); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-900); } .kx-m__acc-count { @@ -209,7 +209,7 @@ border: 1px solid var(--color-neutral-200); background: var(--color-neutral-100); border-radius: var(--radius-lg); - font-size: 20px; + font-size: var(--fs-h2); cursor: pointer; flex-shrink: 0; } @@ -222,7 +222,7 @@ } .kx-m__warn h3 { font-size: var(--fs-body); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-error); margin-bottom: var(--space-2); } @@ -253,7 +253,7 @@ color: var(--color-neutral-900); } .kx-m__rule { - font-size: 11px; + font-size: var(--fs-micro); color: var(--color-neutral-500); background: var(--color-neutral-100); padding: 2px 8px; @@ -269,7 +269,7 @@ border: 1px solid var(--color-neutral-200); border-radius: var(--radius-lg); font-size: var(--fs-h2); - font-weight: 600; + font-weight: var(--fw-semibold); background: var(--color-white); } .kx-m__unit { @@ -283,7 +283,7 @@ .kx-m__field-err { font-size: var(--fs-caption); color: var(--color-error); - font-weight: 600; + font-weight: var(--fw-semibold); } /* 하단 */ @@ -302,7 +302,7 @@ } .kx-m__foot-note { text-align: center; - font-size: 11px; + font-size: var(--fs-micro); color: var(--color-neutral-500); margin-bottom: var(--space-2); } @@ -314,7 +314,7 @@ background: var(--color-primary-600); color: var(--color-on-accent); font-size: var(--fs-h3); - font-weight: 700; + font-weight: var(--fw-bold); cursor: pointer; } .kx-m__cta:disabled { @@ -353,12 +353,12 @@ } .kx-m__compare-head h2 { font-size: var(--fs-body); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-neutral-900); } .kx-m__flag { - font-size: 11px; - font-weight: 700; + font-size: var(--fs-micro); + font-weight: var(--fw-bold); color: var(--color-ai-accent); background: var(--color-ai-surface); border: 1px solid var(--color-ai-accent); @@ -388,7 +388,7 @@ top: 6px; left: 50%; transform: translateX(-50%); - font-size: 10px; + font-size: var(--fs-nano); color: var(--color-neutral-700); background: rgba(255, 255, 255, 0.85); padding: 2px 6px; @@ -399,7 +399,7 @@ .kx-m__compare-div { align-self: center; color: var(--color-neutral-500); - font-size: 14px; + font-size: var(--fs-body); } .kx-m__dots { display: flex; @@ -426,7 +426,7 @@ } .kx-m__ilist h3 { font-size: var(--fs-body); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-neutral-900); padding: 0 var(--space-1); } @@ -458,9 +458,9 @@ color: var(--color-neutral-900); } .kx-m__icard-label small { - font-size: 11px; + font-size: var(--fs-micro); color: var(--color-error); - font-weight: 700; + font-weight: var(--fw-bold); margin-top: 2px; } .kx-m__verdict { @@ -477,7 +477,7 @@ padding: 6px 12px; border-radius: var(--radius-sm); font-size: var(--fs-caption); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-500); cursor: pointer; } diff --git a/src/frontend/src/screens/movein/movein.css b/src/frontend/src/screens/movein/movein.css index 528bde6..5064113 100644 --- a/src/frontend/src/screens/movein/movein.css +++ b/src/frontend/src/screens/movein/movein.css @@ -43,7 +43,7 @@ } .kx-dock__label { font-size: var(--fs-caption); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-500); } .kx-dock__filters-spacer { @@ -86,7 +86,7 @@ padding: 10px; text-align: center; font-size: var(--fs-caption); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-neutral-500); border-right: 1px solid var(--color-neutral-100); display: flex; @@ -101,8 +101,8 @@ .kx-dock__time { padding: 10px 2px; text-align: center; - font-size: 11px; - font-weight: 600; + font-size: var(--fs-micro); + font-weight: var(--fw-semibold); color: var(--color-neutral-500); border-right: 1px solid var(--color-neutral-100); } @@ -122,7 +122,7 @@ align-items: center; justify-content: center; font-size: var(--fs-caption); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-primary-700); border-right: 1px solid var(--color-neutral-100); background: var(--color-neutral-050); @@ -150,7 +150,7 @@ margin: 4px 2px; border-radius: var(--radius-sm); padding: 6px 8px; - font-size: 11px; + font-size: var(--fs-micro); line-height: 1.25; display: flex; align-items: center; @@ -164,7 +164,7 @@ .kx-dock__block--priority { background: var(--color-violation-warn-bg); color: var(--color-violation-warn-text); - font-weight: 700; + font-weight: var(--fw-bold); border: 1px solid var(--color-violation-warn); } .kx-dock__block-text { @@ -280,11 +280,11 @@ } .kx-dock__qr-title { font-size: var(--fs-h3); - font-weight: 700; + font-weight: var(--fw-bold); display: block; } .kx-dock__qr-sub { - font-size: 11px; + font-size: var(--fs-micro); opacity: 0.85; text-transform: uppercase; } @@ -338,7 +338,7 @@ .kx-dock__chart-axis { display: flex; justify-content: space-between; - font-size: 10px; + font-size: var(--fs-nano); color: var(--color-neutral-500); padding: 0 2px; } @@ -425,7 +425,7 @@ gap: 10px; } .kx-logi__item-name { - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-text, #101828); } .kx-logi__item-meta { @@ -464,8 +464,8 @@ color: #b42318; } .kx-logi__pill { - font-size: 12px; - font-weight: 600; + font-size: var(--fs-caption); + font-weight: var(--fw-semibold); padding: 2px 10px; border-radius: 999px; white-space: nowrap; diff --git a/src/frontend/src/screens/ops/ops.css b/src/frontend/src/screens/ops/ops.css index 34851a4..80d640a 100644 --- a/src/frontend/src/screens/ops/ops.css +++ b/src/frontend/src/screens/ops/ops.css @@ -57,7 +57,7 @@ top: var(--space-4); left: var(--space-4); z-index: 2; - font-size: 11px; + font-size: var(--fs-micro); color: var(--color-warning); background: #fff4e5; padding: 3px 10px; @@ -76,7 +76,7 @@ border-radius: var(--radius-lg); } .kx-ops__degraded-badge { - font-size: 11px; + font-size: var(--fs-micro); color: var(--color-warning); background: #fff4e5; padding: 2px 8px; @@ -117,15 +117,15 @@ box-shadow: inset 0 0 0 1px rgba(217, 45, 32, 0.25); } .kx-ops__hall-label { - font-weight: 700; + font-weight: var(--fw-bold); font-size: var(--fs-h3); } .kx-ops__hall-tag { position: absolute; top: var(--space-3); right: var(--space-3); - font-size: 11px; - font-weight: 700; + font-size: var(--fs-micro); + font-weight: var(--fw-bold); color: var(--color-on-accent); background: var(--color-error); padding: 2px 8px; @@ -133,7 +133,7 @@ } .kx-ops__hall-people { font-size: var(--fs-body); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-700); } @@ -233,7 +233,7 @@ } .kx-ops__pill { font-size: var(--fs-caption); - font-weight: 600; + font-weight: var(--fw-semibold); padding: 2px 10px; border-radius: var(--radius-pill); } @@ -294,7 +294,7 @@ /* 주차 */ .kx-ops__note { - font-size: 11px; + font-size: var(--fs-micro); color: var(--color-neutral-500); } .kx-ops__zones { @@ -318,12 +318,12 @@ } .kx-ops__zone-label { font-size: var(--fs-body); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-900); } .kx-ops__zone-pct { font-size: var(--fs-h2); - font-weight: 700; + font-weight: var(--fw-bold); } .kx-ops__zone-pct.is-success { color: var(--color-success); } .kx-ops__zone-pct.is-warning { color: var(--color-warning); } diff --git a/src/frontend/src/screens/public/public.css b/src/frontend/src/screens/public/public.css index b8fbfa0..3e43374 100644 --- a/src/frontend/src/screens/public/public.css +++ b/src/frontend/src/screens/public/public.css @@ -743,7 +743,8 @@ } .kxp-ecard__media { position: relative; - aspect-ratio: 16 / 11; + /* 전시 포스터는 세로형(실측 0.67~0.8) — 세로 프레임으로 맞춰 왜곡/과다크롭 방지. */ + aspect-ratio: 3 / 4; background: var(--color-neutral-100); overflow: hidden; } @@ -4186,6 +4187,10 @@ a.kxp-ai__cite:hover { .kxp-ecard__poster { position: absolute; inset: 0; + width: 100%; + height: 100%; + /* 늘어남(distortion) 방지 — 비율 유지하며 프레임을 채운다. */ + object-fit: cover; } .kxp-ecard__posterfallback { position: absolute; diff --git a/src/frontend/src/screens/schedule/calendarView.css b/src/frontend/src/screens/schedule/calendarView.css index ba9c7ab..ef50055 100644 --- a/src/frontend/src/screens/schedule/calendarView.css +++ b/src/frontend/src/screens/schedule/calendarView.css @@ -38,7 +38,7 @@ } .kx-cv-title { font-size: var(--fs-h3); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-neutral-900); min-width: 160px; text-align: center; @@ -119,7 +119,7 @@ padding: var(--space-2); text-align: center; font-size: var(--fs-caption); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-500); background: var(--color-neutral-050); border-bottom: 1px solid var(--color-neutral-200); @@ -170,14 +170,14 @@ /* 다른 달 날짜 숫자 — 흐린 회색(muted). */ .kx-cv-cell.is-out .kx-cv-daynum { color: var(--color-neutral-500); - font-weight: 500; + font-weight: var(--fw-medium); } /* 오늘 — 진한 아웃라인/알약 제거 → Nifty식 옅은 노랑 셀 틴트 + 숫자 강조. */ .kx-cv-cell.is-today { background: var(--kx-cv-today-bg); } .kx-cv-cell.is-today .kx-cv-daynum { - font-weight: 800; + font-weight: var(--fw-bold); } .kx-cv-cell.is-selected { background: var(--color-primary-050); @@ -188,7 +188,7 @@ left: var(--space-2); height: var(--kx-cv-daynum-h); line-height: var(--kx-cv-daynum-h); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-700); z-index: 2; } @@ -207,8 +207,8 @@ display: inline-block; margin-left: 4px; font-style: normal; - font-size: 11px; - font-weight: 600; + font-size: var(--fs-micro); + font-weight: var(--fw-semibold); color: var(--color-error); max-width: 72px; overflow: hidden; @@ -257,7 +257,7 @@ background: color-mix(in srgb, var(--kx-bar-color) 14%, var(--color-white)); color: color-mix(in srgb, var(--kx-bar-color) 72%, var(--color-neutral-900)); border-left: 3px solid var(--kx-bar-color); - font-size: 11px; + font-size: var(--fs-micro); line-height: var(--kx-cv-bar-h); overflow: hidden; white-space: nowrap; @@ -295,7 +295,7 @@ padding: 0 4px; border-radius: var(--radius-sm); background: color-mix(in srgb, var(--kx-bar-color, var(--color-primary-600)) 22%, transparent); - font-weight: 600; + font-weight: var(--fw-semibold); } .kx-cv-bar:hover .kx-cv-bar-badge { background: rgba(255, 255, 255, 0.25); @@ -312,8 +312,8 @@ padding: 0 4px; border: none; background: transparent; - font-size: 10px; - font-weight: 600; + font-size: var(--fs-nano); + font-weight: var(--fw-semibold); color: var(--color-neutral-500); cursor: pointer; border-radius: var(--radius-sm); @@ -382,7 +382,7 @@ } .kx-cv-day-head { margin-bottom: var(--space-2); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-neutral-900); } .kx-cv-day-list { @@ -405,7 +405,7 @@ background: color-mix(in srgb, var(--kx-bar-color) 14%, var(--color-white)); color: color-mix(in srgb, var(--kx-bar-color) 72%, var(--color-neutral-900)); cursor: pointer; - font-size: 11px; + font-size: var(--fs-micro); line-height: 1.4; overflow: hidden; white-space: nowrap; @@ -461,8 +461,8 @@ flex: 0 0 auto; padding: 1px 6px; border-radius: var(--radius-sm); - font-size: 11px; - font-weight: 600; + font-size: var(--fs-micro); + font-weight: var(--fw-semibold); margin-right: 4px; background: var(--color-neutral-100); color: var(--color-neutral-700); @@ -541,11 +541,11 @@ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - font-weight: 600; + font-weight: var(--fw-semibold); } .kx-cv-more-item-range { flex: 0 0 auto; - font-size: 11px; + font-size: var(--fs-micro); color: var(--color-neutral-500); } @@ -562,7 +562,7 @@ min-height: 120px; } .kx-cv-bar-title { - font-size: 10px; + font-size: var(--fs-nano); } /* 좁은 화면: 툴바 제목 고정폭 해제(가로 오버플로 방지). */ .kx-cv-title { @@ -572,6 +572,6 @@ } .kx-cv-dow { padding: var(--space-1); - font-size: 11px; + font-size: var(--fs-micro); } } diff --git a/src/frontend/src/screens/schedule/schedule.css b/src/frontend/src/screens/schedule/schedule.css index 8af1d0d..c5f23b1 100644 --- a/src/frontend/src/screens/schedule/schedule.css +++ b/src/frontend/src/screens/schedule/schedule.css @@ -125,7 +125,7 @@ /* 행사 상태 pill */ .kx-evstatus { font-size: var(--fs-caption); - font-weight: 600; + font-weight: var(--fw-semibold); padding: 2px 10px; border-radius: var(--radius-pill); white-space: nowrap; @@ -159,7 +159,7 @@ border-radius: var(--radius-sm); background: var(--color-white); color: var(--color-neutral-700); - font-size: 16px; + font-size: var(--fs-h3); cursor: pointer; } .kx-cal__nav:hover { @@ -172,7 +172,7 @@ } .kx-cal__wd { text-align: center; - font-size: 11px; + font-size: var(--fs-micro); color: var(--color-neutral-500); padding-bottom: 4px; } @@ -193,7 +193,7 @@ .kx-cal__cell.has-event { background: var(--color-primary-050); color: var(--color-primary-700); - font-weight: 600; + font-weight: var(--fw-semibold); } .kx-cal__cell.has-event::after { content: ''; @@ -206,13 +206,13 @@ } .kx-cal__cell.is-holiday { color: var(--color-error); - font-weight: 700; + font-weight: var(--fw-bold); } /* today — 진남색 알약 제거 → Nifty식 옅은 노랑 틴트 + 진한 숫자(회색 테두리 유지). */ .kx-cal__cell.is-today { background: var(--color-warning-bg); color: var(--color-primary-700); - font-weight: 700; + font-weight: var(--fw-bold); } .kx-cal__cell.is-today.has-event::after { background: var(--color-primary-600); @@ -263,7 +263,7 @@ .kx-mcal__dow { text-align: center; font-size: var(--fs-caption); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-500); padding: 6px 0; background: var(--color-neutral-050); @@ -299,11 +299,11 @@ color: var(--color-primary-600); } .kx-mcal__cell.is-today .kx-mcal__daynum { - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-primary-700); } .kx-mcal__chip { - font-size: 11px; + font-size: var(--fs-micro); line-height: 1.35; padding: 1px 6px; border-radius: var(--radius-sm); @@ -329,7 +329,7 @@ color: #1f7a54; } .kx-mcal__more { - font-size: 11px; + font-size: var(--fs-micro); color: var(--color-neutral-500); } @@ -363,15 +363,15 @@ grid-template-columns: 1fr auto auto; align-items: center; gap: 8px; - font-size: 12px; + font-size: var(--fs-caption); } .kx-availlist__label { color: var(--kx-text, #0f172a); } .kx-availlist__meta { color: var(--kx-text-muted, #64748b); } .kx-availbadge { - font-size: 11px; + font-size: var(--fs-micro); padding: 2px 8px; border-radius: 999px; - font-weight: 600; + font-weight: var(--fw-semibold); } .kx-availbadge.is-open { color: #15803d; background: #f0fdf4; } .kx-availbadge.is-full { color: #b45309; background: #fffbeb; } diff --git a/src/frontend/src/screens/settlement/settlement.css b/src/frontend/src/screens/settlement/settlement.css index fdf6437..1e0e923 100644 --- a/src/frontend/src/screens/settlement/settlement.css +++ b/src/frontend/src/screens/settlement/settlement.css @@ -18,8 +18,9 @@ } .kx-settle__title { margin: 0; - font-size: 22px; - font-weight: 700; + font-size: var(--fs-h1); + line-height: var(--lh-h1); + font-weight: var(--fw-bold); color: var(--color-neutral-900); } .kx-settle__subtitle { @@ -63,7 +64,7 @@ background: var(--color-white); } .kx-settle__kpi-sub { - font-size: 12px; + font-size: var(--fs-caption); color: var(--color-neutral-500); } .kx-settle__kpi--info { @@ -115,7 +116,7 @@ } .kx-settle__paynote { margin: 12px 0 0; - font-size: 12px; + font-size: var(--fs-caption); color: var(--color-neutral-500); } @@ -161,8 +162,8 @@ display: inline-block; padding: 2px 10px; border-radius: var(--radius-pill); - font-size: 12px; - font-weight: 600; + font-size: var(--fs-caption); + font-weight: var(--fw-semibold); line-height: 18px; } .kx-settle__pill--muted { @@ -210,12 +211,12 @@ } .kx-settle__modal-head h2 { margin: 0; - font-size: 16px; + font-size: var(--fs-h3); } .kx-settle__close { border: none; background: none; - font-size: 16px; + font-size: var(--fs-h3); color: var(--color-neutral-500); cursor: pointer; } @@ -235,13 +236,13 @@ gap: 4px; } .kx-settle__meta-k { - font-size: 12px; + font-size: var(--fs-caption); color: var(--color-neutral-500); } .kx-settle__meta-v { - font-size: 14px; + font-size: var(--fs-body); color: var(--color-neutral-900); - font-weight: 600; + font-weight: var(--fw-semibold); } .kx-settle__lines { @@ -252,14 +253,14 @@ color: var(--color-neutral-700); } .kx-settle__total-row td { - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-neutral-900); border-top: 1px solid var(--color-neutral-200); } .kx-settle__cat { display: inline-block; - font-size: 11px; - font-weight: 600; + font-size: var(--fs-micro); + font-weight: var(--fw-semibold); color: var(--color-primary-700); background: var(--color-primary-050); border-radius: var(--radius-sm); @@ -269,7 +270,7 @@ .kx-settle__pay-hist h3 { margin: 0 0 8px; - font-size: 14px; + font-size: var(--fs-body); } .kx-settle__pay-hist ul { list-style: none; @@ -289,7 +290,7 @@ border-radius: var(--radius-sm); } .kx-settle__pay-method { - font-size: 11px; + font-size: var(--fs-micro); color: var(--color-neutral-500); border: 1px solid var(--color-neutral-200); border-radius: var(--radius-sm); @@ -328,11 +329,11 @@ } .kx-settle__form-error { margin: 0; - font-size: 12px; + font-size: var(--fs-caption); color: var(--color-error); } .kx-settle__muted { - font-size: 12px; + font-size: var(--fs-caption); color: var(--color-neutral-500); text-align: left; } @@ -343,8 +344,8 @@ margin-left: 6px; padding: 1px 7px; border-radius: 999px; - font-size: 11px; - font-weight: 600; + font-size: var(--fs-micro); + font-weight: var(--fw-semibold); vertical-align: middle; white-space: nowrap; } @@ -368,7 +369,7 @@ .kx-settle__tax h3 { margin: 0; font-size: 13px; - font-weight: 600; + font-weight: var(--fw-semibold); } .kx-settle__tax-doc { display: grid; @@ -381,14 +382,14 @@ .kx-settle__tax-row { display: flex; gap: 10px; - font-size: 12px; + font-size: var(--fs-caption); } .kx-settle__tax-amounts { display: flex; flex-wrap: wrap; gap: 14px; margin-top: 4px; - font-size: 12px; + font-size: var(--fs-caption); } .kx-settle__cancel { margin-top: 12px; @@ -404,7 +405,7 @@ .kx-settle__cancel-box h4 { margin: 0; font-size: 13px; - font-weight: 600; + font-weight: var(--fw-semibold); } .kx-settle__refund { display: grid; @@ -414,7 +415,7 @@ display: flex; justify-content: space-between; gap: 12px; - font-size: 12px; + font-size: var(--fs-caption); } .kx-settle__refund-row--em { padding-top: 4px; @@ -455,5 +456,5 @@ .kx-settle__report-group h3 { margin: 0 0 6px; font-size: 13px; - font-weight: 600; + font-weight: var(--fw-semibold); } diff --git a/src/frontend/src/screens/shared.css b/src/frontend/src/screens/shared.css index 7cb758f..4c2793d 100644 --- a/src/frontend/src/screens/shared.css +++ b/src/frontend/src/screens/shared.css @@ -14,13 +14,23 @@ gap: var(--space-5); } -/* 카드 */ +/* ══════════════════════════════════════════════════════════ + * 카드 정본 — Nifty 카드 계보 + * 라운딩 8px · 1px 중립 테두리 · subtle 그림자 · hover elevation. + * 각진/미클립 카드 0 목표: 미디어 카드는 .kx-card--flush + __media 로 모서리 클립. + * ══════════════════════════════════════════════════════════ */ .kx-card { border: var(--border-card); border-radius: var(--radius-lg); background: var(--color-white); padding: var(--space-4); + box-shadow: var(--shadow-card); + transition: box-shadow 0.15s ease, border-color 0.15s ease; } +.kx-card:hover { + box-shadow: var(--shadow-level2); +} +/* 헤더 행(현행) — 제목 + 우측 액션 */ .kx-card__head { display: flex; align-items: center; @@ -38,6 +48,75 @@ margin: -4px 0 var(--space-3); } +/* ── Nifty 구조형 카드 (헤더/바디/푸터·미디어 클립) ── + * .kx-card--flush 로 카드 자체 패딩 제거 + overflow:hidden(미디어를 라운딩에 클립). + * 구획별 패딩·neutral-200 구분선은 각 파트가 담당. */ +.kx-card--flush { + padding: 0; + overflow: hidden; +} +.kx-card__media { + display: block; + width: 100%; + overflow: hidden; + background: var(--color-neutral-100); +} +.kx-card__media > img, +.kx-card__media > video, +.kx-card__media > canvas, +.kx-card__media > svg { + display: block; + width: 100%; + height: 100%; + object-fit: cover; +} +.kx-card__header { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-3); + padding: var(--space-3) var(--space-4); + border-bottom: 1px solid var(--color-neutral-200); +} +.kx-card__header h2, +.kx-card__header h3 { + font-size: var(--fs-h3); + line-height: var(--lh-h3); + color: var(--color-neutral-900); +} +.kx-card__body { + padding: var(--space-4); +} +.kx-card__footer { + display: flex; + align-items: center; + gap: var(--space-2); + padding: var(--space-3) var(--space-4); + border-top: 1px solid var(--color-neutral-200); +} + +/* ══════════════════════════════════════════════════════════ + * 공용 제목 클래스 — 페이지/섹션 제목 편차(20/22/26/30px) 근본 해소. + * 역할↔토큰 1:1 고정. 화면별 제목은 이 클래스로 수렴한다. + * ══════════════════════════════════════════════════════════ */ +.kx-page__title { + font-size: var(--fs-h1); + line-height: var(--lh-h1); + font-weight: var(--fw-bold); + color: var(--color-neutral-900); +} +.kx-page__subtitle { + font-size: var(--fs-body); + line-height: var(--lh-body); + color: var(--color-neutral-500); +} +.kx-section__title { + font-size: var(--fs-h2); + line-height: var(--lh-h2); + font-weight: var(--fw-semibold); + color: var(--color-neutral-900); +} + /* 세그먼트 토글 */ .kx-seg { display: inline-flex; @@ -52,7 +131,7 @@ background: transparent; border-radius: 3px; font-size: var(--fs-body); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-500); cursor: pointer; } @@ -73,7 +152,7 @@ color: var(--color-neutral-700); } -/* KPI 카드(공통) */ +/* KPI 카드(공통) — 카드 정본과 동일 Nifty 엘리베이션 */ .kx-kpi { display: flex; flex-direction: column; @@ -82,6 +161,11 @@ border: var(--border-card); border-radius: var(--radius-lg); background: var(--color-white); + box-shadow: var(--shadow-card); + transition: box-shadow 0.15s ease; +} +.kx-kpi:hover { + box-shadow: var(--shadow-level2); } .kx-kpi__label { display: flex; @@ -92,13 +176,13 @@ } .kx-kpi__ai { color: var(--color-ai-accent); - font-weight: 700; - font-size: 11px; + font-weight: var(--fw-bold); + font-size: var(--fs-micro); } .kx-kpi__value { font-size: var(--fs-display); line-height: var(--lh-display); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-neutral-900); } .kx-kpi__sub { @@ -116,7 +200,7 @@ .kx-delta { align-self: flex-start; font-size: var(--fs-caption); - font-weight: 700; + font-weight: var(--fw-bold); padding: 1px 8px; border-radius: var(--radius-pill); } @@ -131,7 +215,7 @@ /* degraded 배지 */ .kx-bi__degraded { - font-size: 11px; + font-size: var(--fs-micro); color: var(--color-warning); background: var(--color-warning-bg); padding: 2px 8px; @@ -160,7 +244,7 @@ z-index: 1; padding: 10px 12px; font-size: var(--fs-caption); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-500); text-align: left; letter-spacing: 0.02em; @@ -190,6 +274,41 @@ font-variant-numeric: tabular-nums; } +/* ── 비교 매트릭스 표준 (.kx-matrix) — 목록 표준과 분리된 의도적 패턴. + * 헤더 강조(neutral-900/700)·중앙 정렬·항목별 1px 셀 보더. auction 낙찰 비교 등. */ +.kx-matrix { + width: 100%; + border-collapse: collapse; + font-size: var(--fs-body); + color: var(--color-neutral-700); +} +.kx-matrix th, +.kx-matrix td { + padding: var(--space-3) var(--space-4); + border: 1px solid var(--color-neutral-200); + text-align: center; + vertical-align: middle; +} +.kx-matrix thead th { + font-size: var(--fs-caption); + font-weight: var(--fw-bold); + color: var(--color-neutral-900); + background: var(--color-neutral-050); + white-space: nowrap; +} +.kx-matrix tbody th { + font-weight: var(--fw-semibold); + color: var(--color-neutral-900); + text-align: left; + background: var(--color-neutral-050); +} +.kx-matrix .kx-num { + font-variant-numeric: tabular-nums; +} + +/* 참고: docs 리포트 표(.kx-rep__table)는 A4 종이 시뮬레이션(고정 흰 배경·고정 잉크색)이라 + * 테마 토큰(다크서 반전) 부적합 + 신규 인쇄색 토큰 금지 → 의도적 테마 독립 예외로 유지한다. */ + /* 가로 막대 리스트 */ .kx-barlist { list-style: none; @@ -223,7 +342,7 @@ .kx-grade { display: inline-block; font-size: var(--fs-caption); - font-weight: 600; + font-weight: var(--fw-semibold); padding: 2px 10px; border-radius: var(--radius-pill); } diff --git a/src/frontend/src/screens/styleguide/styleguide.css b/src/frontend/src/screens/styleguide/styleguide.css index ec74789..9200be2 100644 --- a/src/frontend/src/screens/styleguide/styleguide.css +++ b/src/frontend/src/screens/styleguide/styleguide.css @@ -20,7 +20,7 @@ } .kx-sg__eyebrow { font-size: var(--fs-caption); - font-weight: 700; + font-weight: var(--fw-bold); letter-spacing: 0.06em; text-transform: uppercase; color: var(--color-primary-600); @@ -92,9 +92,9 @@ border-bottom: var(--border-card); margin-bottom: var(--space-4); } -.kx-sg__t-display { font-size: var(--fs-display); line-height: var(--lh-display); font-weight: 700; color: var(--color-neutral-900); } -.kx-sg__t-headline { font-size: var(--fs-h1); line-height: var(--lh-h1); font-weight: 700; color: var(--color-neutral-900); } -.kx-sg__t-title { font-size: var(--fs-h3); font-weight: 600; color: var(--color-neutral-700); } +.kx-sg__t-display { font-size: var(--fs-display); line-height: var(--lh-display); font-weight: var(--fw-bold); color: var(--color-neutral-900); } +.kx-sg__t-headline { font-size: var(--fs-h1); line-height: var(--lh-h1); font-weight: var(--fw-bold); color: var(--color-neutral-900); } +.kx-sg__t-title { font-size: var(--fs-h3); font-weight: var(--fw-semibold); color: var(--color-neutral-700); } .kx-sg__t-body { font-size: var(--fs-body); color: var(--color-neutral-700); } .kx-sg__swatches { @@ -120,7 +120,7 @@ flex: 1; } .kx-sg__swatch-hex { - font-size: 11px; + font-size: var(--fs-micro); color: var(--color-neutral-500); } @@ -144,7 +144,7 @@ } .kx-sg__viol { font-size: var(--fs-caption); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-error); background: #fef3f2; border-radius: var(--radius-sm); @@ -225,7 +225,7 @@ .kx-sg__err-msg { color: var(--color-error); font-style: normal; - font-size: 11px; + font-size: var(--fs-micro); } .kx-sg__check-row { grid-column: 1 / -1; @@ -254,7 +254,7 @@ background: #fef3f2; color: var(--color-error); font-size: var(--fs-body); - font-weight: 600; + font-weight: var(--fw-semibold); padding: 10px var(--space-4); border-radius: var(--radius-sm); border: 1px solid rgba(217, 45, 32, 0.3); diff --git a/src/frontend/src/screens/utility/utility-order.css b/src/frontend/src/screens/utility/utility-order.css index 7d1c6a6..c95b861 100644 --- a/src/frontend/src/screens/utility/utility-order.css +++ b/src/frontend/src/screens/utility/utility-order.css @@ -29,7 +29,7 @@ background: var(--color-white); border-radius: var(--radius-sm); cursor: pointer; - font-size: 16px; + font-size: var(--fs-h3); color: var(--color-neutral-700); } .kx-uord__wizard { @@ -55,8 +55,8 @@ display: flex; align-items: center; justify-content: center; - font-size: 11px; - font-weight: 700; + font-size: var(--fs-micro); + font-weight: var(--fw-bold); background: var(--color-neutral-200); color: var(--color-neutral-500); } @@ -67,7 +67,7 @@ } .kx-uord__wizard li.is-current { color: var(--color-primary-700); - font-weight: 600; + font-weight: var(--fw-semibold); } .kx-uord__wizard li.is-current::before { background: var(--color-primary-600); @@ -119,8 +119,8 @@ position: absolute; bottom: var(--space-3); right: var(--space-3); - font-size: 11px; - font-weight: 700; + font-size: var(--fs-micro); + font-weight: var(--fw-bold); color: var(--color-primary-700); background: rgba(255, 255, 255, 0.9); border: 1px solid var(--color-primary-100); diff --git a/src/frontend/src/screens/utility/utility.css b/src/frontend/src/screens/utility/utility.css index 4272b0a..9859660 100644 --- a/src/frontend/src/screens/utility/utility.css +++ b/src/frontend/src/screens/utility/utility.css @@ -26,7 +26,7 @@ padding: 4px 12px; border-radius: var(--radius-sm); font-size: var(--fs-caption); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-violation-warn-text); background: var(--color-violation-warn-bg); } @@ -43,7 +43,7 @@ padding: 6px 14px; border-radius: var(--radius-sm); font-size: var(--fs-body); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-500); } .kx-util__tab.is-active { @@ -110,7 +110,7 @@ border: none; background: transparent; color: var(--color-neutral-500); - font-size: 12px; + font-size: var(--fs-caption); } .kx-devtable__del:hover { color: var(--color-error); @@ -123,7 +123,7 @@ padding: 6px 12px; border-radius: var(--radius-sm); font-size: var(--fs-caption); - font-weight: 600; + font-weight: var(--fw-semibold); } .kx-util__compute { @@ -200,7 +200,7 @@ font-style: normal; } .kx-quote__amount { - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-900); } .kx-quote__total { @@ -216,12 +216,12 @@ color: var(--color-primary-700); } .kx-quote__disclaimer { - font-size: 11px; + font-size: var(--fs-micro); line-height: 15px; color: var(--color-neutral-500); } .kx-quote__ruleset { - font-size: 11px; + font-size: var(--fs-micro); color: var(--color-neutral-500); } .kx-quote__empty { @@ -239,14 +239,14 @@ gap: 4px; } .kx-quote__submitted-title { - font-weight: 700; + font-weight: var(--fw-bold); font-size: var(--fs-body); } .kx-quote__relay { color: var(--color-neutral-700); } .kx-quote__diagram { - font-weight: 600; + font-weight: var(--fw-semibold); } .kx-util__banner { font-size: var(--fs-caption); diff --git a/src/frontend/src/screens/utility/wiring.css b/src/frontend/src/screens/utility/wiring.css index 675dcb2..aa54a09 100644 --- a/src/frontend/src/screens/utility/wiring.css +++ b/src/frontend/src/screens/utility/wiring.css @@ -22,7 +22,7 @@ padding: 4px 10px; border-radius: var(--radius-pill); font-size: var(--fs-caption); - font-weight: 600; + font-weight: var(--fw-semibold); color: #fde68a; background: rgba(180, 83, 9, 0.28); border: 1px solid rgba(253, 224, 71, 0.4); @@ -102,7 +102,7 @@ .kx-wire__path-label { fill: var(--color-white); font-size: 1.4px; - font-weight: 600; + font-weight: var(--fw-semibold); text-anchor: middle; paint-order: stroke; stroke: rgba(16, 24, 40, 0.85); diff --git a/src/frontend/src/screens/visitor/checkin.css b/src/frontend/src/screens/visitor/checkin.css index 7fdd22e..443a255 100644 --- a/src/frontend/src/screens/visitor/checkin.css +++ b/src/frontend/src/screens/visitor/checkin.css @@ -316,5 +316,5 @@ font-size: var(--fs-caption); color: var(--color-neutral-500); } -.kx-checkin__inside-sub b { color: var(--color-neutral-700); font-weight: 600; } +.kx-checkin__inside-sub b { color: var(--color-neutral-700); font-weight: var(--fw-semibold); } .kx-checkin__hours { min-width: 0; } diff --git a/src/frontend/src/screens/visitor/visitor.css b/src/frontend/src/screens/visitor/visitor.css index 52ea10d..cde74f6 100644 --- a/src/frontend/src/screens/visitor/visitor.css +++ b/src/frontend/src/screens/visitor/visitor.css @@ -38,8 +38,8 @@ /* 샘플 데이터 배지 */ .kx-sample { - font-size: 11px; - font-weight: 700; + font-size: var(--fs-micro); + font-weight: var(--fw-bold); color: var(--color-warning); background: #fff4e5; border: 1px solid #fcd9a8; @@ -50,8 +50,8 @@ /* 오프라인 강등(폴백) 표시 — 집계 API 미연결 시 시연 데이터 안내 */ .kx-vis__degraded { - font-size: 11px; - font-weight: 700; + font-size: var(--fs-micro); + font-weight: var(--fw-bold); color: var(--color-warning); background: #fff4e5; border: 1px solid #fcd9a8; @@ -139,7 +139,7 @@ } .kx-vis__donut-title { font-size: var(--fs-caption); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-700); margin-bottom: var(--space-2); } @@ -202,8 +202,8 @@ color: var(--color-neutral-500); } .kx-vis__form-state { - font-size: 11px; - font-weight: 600; + font-size: var(--fs-micro); + font-weight: var(--fw-semibold); color: var(--color-neutral-500); background: var(--color-neutral-050); border: 1px solid var(--color-neutral-200); @@ -250,7 +250,7 @@ background: var(--color-primary-600); color: var(--color-on-accent); font-size: 8px; - font-weight: 700; + font-weight: var(--fw-bold); letter-spacing: 0.14em; text-align: center; padding: 5px 0; @@ -261,7 +261,7 @@ } .kx-badgecard__name { font-size: var(--fs-caption); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-primary-700); } .kx-badgecard__org { @@ -279,7 +279,7 @@ } .kx-badgecard__tag { font-size: 7px; - font-weight: 800; + font-weight: var(--fw-bold); color: var(--color-on-accent); background: var(--color-ai-accent); padding: 2px 5px; @@ -295,7 +295,7 @@ overflow: hidden; } .kx-vis__pii-note { - font-size: 11px; + font-size: var(--fs-micro); color: var(--color-neutral-500); } .kx-vis__person { @@ -303,7 +303,7 @@ align-items: center; gap: var(--space-3); color: var(--color-neutral-900); - font-weight: 500; + font-weight: var(--fw-medium); } .kx-vis__avatar { width: 30px; @@ -313,7 +313,7 @@ background: var(--color-primary-100); color: var(--color-primary-700); font-size: var(--fs-caption); - font-weight: 700; + font-weight: var(--fw-bold); display: inline-flex; align-items: center; justify-content: center; @@ -328,8 +328,8 @@ /* 유형 pill */ .kx-vis__type { display: inline-block; - font-size: 11px; - font-weight: 700; + font-size: var(--fs-micro); + font-weight: var(--fw-bold); padding: 2px 10px; border-radius: var(--radius-pill); } @@ -352,7 +352,7 @@ align-items: center; gap: 6px; font-size: var(--fs-body); - font-weight: 600; + font-weight: var(--fw-semibold); } .kx-vis__checkin.is-done { color: var(--color-success); @@ -379,7 +379,7 @@ gap: 5px; font-size: var(--fs-caption); color: var(--color-primary-600); - font-weight: 600; + font-weight: var(--fw-semibold); } .kx-vis__badge-none { font-size: var(--fs-caption); @@ -425,7 +425,7 @@ align-items: center; gap: 4px; font-size: var(--fs-caption); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-700); background: var(--color-neutral-100); border: 1px solid transparent; @@ -450,7 +450,7 @@ outline-offset: -2px; } .kx-vis__lead-name { - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-neutral-900); } @@ -488,8 +488,8 @@ background: var(--color-ai-accent); } .kx-gauge__num { - font-size: 11px; - font-weight: 700; + font-size: var(--fs-micro); + font-weight: var(--fw-bold); color: var(--color-on-accent); background: var(--color-primary-600); border-radius: var(--radius-pill); @@ -523,8 +523,8 @@ color: var(--color-neutral-900); } .kx-vis__hot { - font-size: 10px; - font-weight: 800; + font-size: var(--fs-nano); + font-weight: var(--fw-bold); color: #4500d8; background: #e9e3ff; padding: 2px 7px; @@ -557,7 +557,7 @@ .kx-vis__ai-score { font-size: var(--fs-display); line-height: 1; - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-ai-accent); } .kx-vis__ai-actions { @@ -588,7 +588,7 @@ } .kx-vis__ai-check { color: var(--color-ai-accent); - font-weight: 700; + font-weight: var(--fw-bold); flex-shrink: 0; } @@ -600,7 +600,7 @@ align-items: center; gap: 6px; font-size: var(--fs-caption); - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-neutral-900); margin-bottom: var(--space-2); } diff --git a/src/frontend/src/screens/work/work.css b/src/frontend/src/screens/work/work.css index 43fcc5c..85c8879 100644 --- a/src/frontend/src/screens/work/work.css +++ b/src/frontend/src/screens/work/work.css @@ -61,7 +61,7 @@ } .kx-label { font-size: var(--fs-caption); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-500); } .kx-input, @@ -104,7 +104,7 @@ text-align: left; padding: 10px 12px; font-size: var(--fs-caption); - font-weight: 600; + font-weight: var(--fw-semibold); letter-spacing: 0.02em; color: var(--color-neutral-500); border-bottom: 2px solid var(--color-neutral-200); @@ -130,7 +130,7 @@ background: var(--color-primary-100); } .kx-list-table__title { - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-900); } .kx-list-table__muted { @@ -149,7 +149,7 @@ padding: 1px 10px; border-radius: var(--radius-pill); font-size: var(--fs-caption); - font-weight: 600; + font-weight: var(--fw-semibold); white-space: nowrap; } .kx-pill--neutral { @@ -246,7 +246,7 @@ color: var(--color-neutral-500); } .kx-thread__author { - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-700); } .kx-thread__text { @@ -297,7 +297,7 @@ background: transparent; border-radius: var(--radius-sm); font-size: var(--fs-body); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-700); cursor: pointer; text-align: left; @@ -338,7 +338,7 @@ background: var(--color-primary-100); } .kx-msg__item.is-unread .kx-msg__item-title { - font-weight: 700; + font-weight: var(--fw-bold); color: var(--color-neutral-900); } .kx-msg__item-top { @@ -396,7 +396,7 @@ border-radius: var(--radius-pill); padding: 2px 6px 2px 10px; font-size: var(--fs-caption); - font-weight: 600; + font-weight: var(--fw-semibold); } .kx-tag button { border: none; @@ -445,7 +445,7 @@ padding: 8px; text-align: center; font-size: var(--fs-caption); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-500); } .kx-cal__wd--sun { @@ -474,7 +474,7 @@ } .kx-cal__daynum { font-size: var(--fs-caption); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-700); } .kx-cal__cell.is-today .kx-cal__daynum { @@ -488,7 +488,7 @@ justify-content: center; } .kx-cal__event { - font-size: 11px; + font-size: var(--fs-micro); line-height: 16px; padding: 1px 6px; border-radius: 4px; @@ -498,7 +498,7 @@ white-space: nowrap; } .kx-cal__more { - font-size: 11px; + font-size: var(--fs-micro); color: var(--color-neutral-500); } .kx-cal__legend { @@ -575,7 +575,7 @@ background: var(--color-ai-surface); color: var(--color-ai-accent); font-size: var(--fs-caption); - font-weight: 700; + font-weight: var(--fw-bold); cursor: pointer; white-space: nowrap; } @@ -595,7 +595,7 @@ border: none; background: transparent; font-size: var(--fs-body); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-500); cursor: pointer; border-bottom: 2px solid transparent; @@ -696,7 +696,7 @@ border: var(--border-card); background: var(--color-white); font-size: var(--fs-caption); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-700); cursor: pointer; } @@ -731,7 +731,7 @@ min-width: 0; } .kx-noti__title { - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-900); font-size: var(--fs-body); } @@ -782,7 +782,7 @@ background: transparent; border-radius: var(--radius-sm); font-size: var(--fs-body); - font-weight: 600; + font-weight: var(--fw-semibold); color: var(--color-neutral-700); cursor: pointer; } @@ -797,7 +797,7 @@ background: var(--color-primary-600); color: #fff; font-size: 26px; - font-weight: 700; + font-weight: var(--fw-bold); display: flex; align-items: center; justify-content: center; diff --git a/src/frontend/src/styles/global.css b/src/frontend/src/styles/global.css index 59a0575..ee601b5 100644 --- a/src/frontend/src/styles/global.css +++ b/src/frontend/src/styles/global.css @@ -75,6 +75,7 @@ --ai-skeleton-a: var(--dk-ai-skeleton-a); --ai-skeleton-b: var(--dk-ai-skeleton-b); --focus-ring: var(--dk-primary-700); + --shadow-card: 0 1px 2px rgba(0, 0, 0, 0.4); --shadow-level2: 0 4px 12px rgba(0, 0, 0, 0.5); } @@ -113,6 +114,7 @@ --ai-skeleton-a: var(--dk-ai-skeleton-a); --ai-skeleton-b: var(--dk-ai-skeleton-b); --focus-ring: var(--dk-primary-700); + --shadow-card: 0 1px 2px rgba(0, 0, 0, 0.4); --shadow-level2: 0 4px 12px rgba(0, 0, 0, 0.5); } } diff --git a/src/frontend/src/styles/tokens.css b/src/frontend/src/styles/tokens.css index e35dbbe..7a46df2 100644 --- a/src/frontend/src/styles/tokens.css +++ b/src/frontend/src/styles/tokens.css @@ -66,8 +66,19 @@ --lh-body: 22px; --fs-caption: 12px; --lh-caption: 18px; + /* 마이크로 라벨(비공식 계층 공식화) — audit: 11px 128건 흡수 */ + --fs-micro: 11px; + --lh-micro: 16px; + /* 초소형(배지·QR·범례) — 10/9/8px 수렴 */ + --fs-nano: 10px; --fs-mono: 13px; + /* ── 폰트 웨이트 토큰 (audit P0) — 페이지 간 700/600/500/800 혼용 수렴 ── */ + --fw-regular: 400; + --fw-medium: 500; + --fw-semibold: 600; + --fw-bold: 700; /* 800 남발 → 700 수렴 (display/hero 예외만 인라인) */ + /* ── 라운드 (§1-4) — 최대 8px, 칩은 pill ── */ --radius-sm: 4px; /* 버튼·인풋·작은 요소 */ --radius-lg: 8px; /* 카드·모달·섹션 */ @@ -75,7 +86,8 @@ /* ── 엘리베이션 — 톤 레이어 + 1px 테두리 ── */ --border-card: 1px solid var(--color-neutral-200); - --shadow-level2: 0 4px 12px rgba(16, 24, 40, 0.1); + --shadow-card: 0 1px 2px rgba(16, 24, 40, 0.06); /* Nifty 카드 기본(subtle) */ + --shadow-level2: 0 4px 12px rgba(16, 24, 40, 0.1); /* 카드 hover 상승 */ --accent-ai: 4px solid var(--color-ai-accent); /* AI 카드 좌측 액센트 */ /* ── 그리드·간격 (§1-4) — 4px 베이스 ── */
콘텐츠 / 원문(KO)