@ihabdevteam/core 0.200.0 → 0.201.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +111 -0
- package/dist/components.cjs.js +177 -177
- package/dist/components.esm.css +1 -1
- package/dist/hooks.cjs.js +3 -3
- package/dist/i18n.cjs.js +3 -3
- package/dist/ihabdevteam-core.cjs.js +186 -186
- package/dist/ihabdevteam-core.d.ts +206 -18
- package/dist/packages/core/src/components/Header.css +139 -118
- package/dist/packages/core/src/components/Popup/useDiscardGuard.d.ts +48 -0
- package/dist/packages/core/src/components/Popup/useDiscardGuard.js +56 -0
- package/dist/packages/core/src/components.d.ts +2 -0
- package/dist/packages/core/src/components.js +1 -0
- package/dist/packages/core/src/hooks/useListControls.d.ts +66 -0
- package/dist/packages/core/src/hooks/useListControls.js +52 -0
- package/dist/packages/core/src/hooks.d.ts +1 -0
- package/dist/packages/core/src/hooks.js +1 -0
- package/dist/packages/core/src/i18n/locales/en.json +5 -0
- package/dist/packages/core/src/i18n/locales/ko.json +5 -0
- package/dist/packages/core/src/utils/chunkSkew.d.ts +27 -0
- package/dist/packages/core/src/utils/chunkSkew.js +44 -0
- package/dist/packages/core/src/utils/format.d.ts +14 -14
- package/dist/packages/core/src/utils/format.js +27 -1
- package/dist/packages/core/src/utils/sessionTab.d.ts +36 -0
- package/dist/packages/core/src/utils/sessionTab.js +36 -0
- package/dist/packages/core/src/utils.d.ts +4 -2
- package/dist/packages/core/src/utils.js +3 -2
- package/dist/packages/core/src/version.d.ts +1 -1
- package/dist/packages/core/src/version.js +1 -1
- package/dist/utils.cjs.js +2 -2
- package/package.json +1 -1
|
@@ -1626,7 +1626,7 @@ export declare function copyText(text: string): Promise<boolean>;
|
|
|
1626
1626
|
*
|
|
1627
1627
|
* 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
|
|
1628
1628
|
*/
|
|
1629
|
-
export declare const CORE_VERSION = "0.
|
|
1629
|
+
export declare const CORE_VERSION = "0.201.0";
|
|
1630
1630
|
|
|
1631
1631
|
/** @public */
|
|
1632
1632
|
declare const coreDefault: Record<string, unknown>;
|
|
@@ -2042,6 +2042,23 @@ export declare type DialogSize = 'sm' | 'md' | 'lg' | 'full';
|
|
|
2042
2042
|
*/
|
|
2043
2043
|
declare type DifficultyLevel = 'random' | 'jong' | 'jung' | 'cho' | 'count';
|
|
2044
2044
|
|
|
2045
|
+
/** @public */
|
|
2046
|
+
export declare interface DiscardGuard {
|
|
2047
|
+
/** `Popup` 의 `beforeClose` 로 넘긴다 — `false` 면 안 닫힌다 */
|
|
2048
|
+
beforeClose: () => boolean;
|
|
2049
|
+
/** 물음창. JSX 어디에 두든 된다(아래 글 참고) */
|
|
2050
|
+
discardPopup: ReactNode;
|
|
2051
|
+
/** **다음 한 번은 안 묻는다** — 저장이 끝나고 스스로 닫을 때 부른다 */
|
|
2052
|
+
allowClose: () => void;
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
/** @public */
|
|
2056
|
+
export declare interface DiscardGuardLabels {
|
|
2057
|
+
title?: string;
|
|
2058
|
+
message?: string;
|
|
2059
|
+
confirmLabel?: string;
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2045
2062
|
/** @public */
|
|
2046
2063
|
export declare function Divider({ orientation, label, className }: DividerProps): JSX.Element;
|
|
2047
2064
|
|
|
@@ -2980,6 +2997,34 @@ export declare function injectPassCardBgTheme(el: HTMLElement | null, theme: Pas
|
|
|
2980
2997
|
/** theme 객체를 부모 요소에 CSS var로 주입 */
|
|
2981
2998
|
export declare function injectPromoBannerBgTheme(el: HTMLElement | null, theme: PromoBannerBgTheme): void;
|
|
2982
2999
|
|
|
3000
|
+
/**
|
|
3001
|
+
* **위 복구를 창에 매단다** — 듣는 자리 둘을 한 번에 달고, 떼는 함수를 돌려준다.
|
|
3002
|
+
*
|
|
3003
|
+
* 복구하는 일(`recoverFromSkew`)은 코어에 있었는데 **어디에 어떻게 매다는지**가 소비자마다
|
|
3004
|
+
* 한 벌씩이었다(말귀 회원앱·어드민의 `main.tsx`, 글자까지 같았다 — 2026-09-27). 그 열몇 줄에
|
|
3005
|
+
* 조용히 틀어지는 조각이 둘 있다:
|
|
3006
|
+
*
|
|
3007
|
+
* 1. **`error` 는 캡처 단계로 들어야 한다**(셋째 인자 `true`). `link`·`style` 의 `error` 는
|
|
3008
|
+
* **버블링하지 않아** 창까지 올라오지 않는다. 한쪽에서 그 `true` 를 빠뜨리면 그 앱만 CSS
|
|
3009
|
+
* 청크 실패를 못 듣고, **화면은 뜨는데 스타일만 전부 빠진 채** 남는다(말귀 실제 QA 건 —
|
|
3010
|
+
* 매니저 화면이 기본 스타일로 무너졌다). 스크립트는 멀쩡히 도니 오류도 안 난다.
|
|
3011
|
+
* 2. **`vite:preloadError` 는 막아야(`preventDefault`)** Vite 헬퍼가 오류를 안 던진다.
|
|
3012
|
+
* 막지 않으면 새로고침과 오류 화면이 함께 난다(이 파일 머리말).
|
|
3013
|
+
*
|
|
3014
|
+
* CSS 쪽은 `preventDefault` 를 하지 않는다 — 막을 기본 동작이 없고, 그 오류는 다른
|
|
3015
|
+
* 듣는 이(오류 보고 등)에게도 그대로 가는 편이 맞다.
|
|
3016
|
+
*
|
|
3017
|
+
* ```ts
|
|
3018
|
+
* // main.tsx — 한 줄이면 된다
|
|
3019
|
+
* installSkewRecovery()
|
|
3020
|
+
* ```
|
|
3021
|
+
*
|
|
3022
|
+
* 창이 없는 곳(SSR·시험)에서는 아무것도 안 달고 빈 함수를 돌려준다.
|
|
3023
|
+
* @returns 떼는 함수. 앱 시작점에서는 뗄 일이 없지만, 시험이 뒷정리할 수 있어야 한다.
|
|
3024
|
+
* @public
|
|
3025
|
+
*/
|
|
3026
|
+
export declare function installSkewRecovery(): () => void;
|
|
3027
|
+
|
|
2983
3028
|
/** @public 검색어가 초성으로만 이루어졌는지 (예: 'ㅎㅈ' → true, '한' → false) */
|
|
2984
3029
|
export declare function isAllChoseong(search: string): boolean;
|
|
2985
3030
|
|
|
@@ -3005,6 +3050,13 @@ export declare function isManualOnlyType(typeKey: WordTrainingTypeKey): boolean;
|
|
|
3005
3050
|
*/
|
|
3006
3051
|
export declare function isNetworkErrorMessage(msg: string | null | undefined): boolean;
|
|
3007
3052
|
|
|
3053
|
+
/**
|
|
3054
|
+
* 연락처가 **사람에게 거는 번호**의 모양인가 — 대표번호(15xx~19xx)는 안 받는다.
|
|
3055
|
+
* 규칙은 `PERSONAL_PHONE_REGEX`, 대표번호까지 받으려면 `isValidPhone`.
|
|
3056
|
+
* @public
|
|
3057
|
+
*/
|
|
3058
|
+
export declare function isPersonalPhone(phone: string): boolean;
|
|
3059
|
+
|
|
3008
3060
|
/** @public */
|
|
3009
3061
|
export declare function isSelectionCorrect({ payload, selected, choices, correctAnswers }: IsSelectionCorrectParams): boolean | undefined;
|
|
3010
3062
|
|
|
@@ -3015,6 +3067,39 @@ declare interface IsSelectionCorrectParams {
|
|
|
3015
3067
|
correctAnswers?: unknown[];
|
|
3016
3068
|
}
|
|
3017
3069
|
|
|
3070
|
+
/**
|
|
3071
|
+
* **이 이벤트가 이 탭에 실제로 반영된 것인가.**
|
|
3072
|
+
*
|
|
3073
|
+
* 로그인을 탭마다 가르려고 `auth.storage` 를 `sessionStorage` 로 두어도, GoTrue 의
|
|
3074
|
+
* `BroadcastChannel` 은 **저장소 종류와 무관하게** 세션 이벤트를 같은 브라우저의 모든 탭에
|
|
3075
|
+
* 뿌린다(여러 탭을 맞추라고 만든 길이다). 그래서 받은 세션을 그대로 믿으면 **다른 탭에서
|
|
3076
|
+
* 로그인한 세션이 이 탭 화면에 얹힌다** — 로그인된 것처럼 그려지는데 이 탭의 저장소는 비어
|
|
3077
|
+
* 있어 API 호출은 전부 미인증으로 실패한다. 반대로 이 탭에 제 세션이 있는데 다른 탭의
|
|
3078
|
+
* 로그아웃이 날아와 뒤섞이기도 한다. 말귀 어드민에서는 그렇게 얹히는 것이 `ROLE_ADMIN`
|
|
3079
|
+
* 세션이다.
|
|
3080
|
+
*
|
|
3081
|
+
* 가르는 법은 하나다 — `getSession()` 으로 **이 탭 저장소를 다시 읽어** 받은 것과 맞대 본다.
|
|
3082
|
+
* 그 한 줄과 여덟 줄짜리 까닭이 말귀 회원앱·어드민에 한 벌씩 있었다(2026-09-27).
|
|
3083
|
+
*
|
|
3084
|
+
* **로그아웃(세션 없음)은 늘 받는다.** 지울 것이 없다는 말은 어느 탭에서 왔든 참이고,
|
|
3085
|
+
* 여기서 막으면 이 탭이 로그인된 채로 남는다 — 막는 것보다 나쁜 쪽이다.
|
|
3086
|
+
*
|
|
3087
|
+
* ```ts
|
|
3088
|
+
* onAuthStateChange((event, 받은것) => {
|
|
3089
|
+
* getSession().then(({ data }) => {
|
|
3090
|
+
* const 이탭것 = data.session
|
|
3091
|
+
* if (!isSessionForThisTab(받은것, 이탭것)) return // 다른 탭에서 온 것 — 무시
|
|
3092
|
+
* …이탭것 으로 상태를 맞춘다
|
|
3093
|
+
* })
|
|
3094
|
+
* })
|
|
3095
|
+
* ```
|
|
3096
|
+
*
|
|
3097
|
+
* @param 받은것 이벤트가 실어 온 세션(없을 수 있다)
|
|
3098
|
+
* @param 이탭것 `getSession()` 이 이 탭 저장소에서 다시 읽은 세션
|
|
3099
|
+
* @public
|
|
3100
|
+
*/
|
|
3101
|
+
export declare function isSessionForThisTab(받은것: TabSessionLike | null | undefined, 이탭것: TabSessionLike | null | undefined): boolean;
|
|
3102
|
+
|
|
3018
3103
|
/**
|
|
3019
3104
|
* @public 문자열이 음절 패턴 DSL 문법인지 — 쉼표(,) 포함 여부로 판별한다.
|
|
3020
3105
|
* 일반 단어 검색어엔 쉼표가 쓰이지 않으므로 충돌 없이 자동 분기할 수 있다.
|
|
@@ -3433,6 +3518,37 @@ export declare interface LineChartScales {
|
|
|
3433
3518
|
};
|
|
3434
3519
|
}
|
|
3435
3520
|
|
|
3521
|
+
/** @public */
|
|
3522
|
+
export declare interface ListControls {
|
|
3523
|
+
/** 찾는 말. 무엇을 어떻게 거를지는 **부르는 쪽**이 정한다 — 이 훅은 그 말만 쥔다 */
|
|
3524
|
+
query: string;
|
|
3525
|
+
setQuery: (v: string) => void;
|
|
3526
|
+
/** 고르개에 그대로 주는 글 */
|
|
3527
|
+
pageSize: string;
|
|
3528
|
+
setPageSize: (v: string) => void;
|
|
3529
|
+
/** `usePagedList` 에 그대로 주는 수 — `Number(pageSize)` 가 못 읽히면 기본값이다 */
|
|
3530
|
+
perPage: number;
|
|
3531
|
+
page: number;
|
|
3532
|
+
setPage: (n: number) => void;
|
|
3533
|
+
}
|
|
3534
|
+
|
|
3535
|
+
/** @public */
|
|
3536
|
+
export declare interface ListControlsOptions {
|
|
3537
|
+
/**
|
|
3538
|
+
* 쪽크기 기본값. 글인 것은 고르개(`Dropdown`)가 글을 다루기 때문이다 — 숫자로 바꾸는 일은
|
|
3539
|
+
* 이 훅이 한다(`perPage`). 안 주면 `'25'`.
|
|
3540
|
+
*/
|
|
3541
|
+
defaultPageSize?: string;
|
|
3542
|
+
/**
|
|
3543
|
+
* 검색어·쪽크기 **말고 더** 바뀌면 첫 쪽으로 돌아가야 하는 것들 — 갈래(`type`) · 보기(`view`) 처럼
|
|
3544
|
+
* 목록을 거르는 값이다. 검색어와 쪽크기는 이 훅이 스스로 넣으므로 **여기 또 적지 않는다.**
|
|
3545
|
+
*
|
|
3546
|
+
* 값은 글·수·참거짓만 받는다. 객체를 넣으면 `String()` 이 전부 `[object Object]` 로 만들어
|
|
3547
|
+
* **무엇이 바뀌어도 안 바뀐 것이 되고, 그래도 아무 데서도 안 터진다** — 타입으로 막는다.
|
|
3548
|
+
*/
|
|
3549
|
+
resetOn?: readonly ListResetValue[];
|
|
3550
|
+
}
|
|
3551
|
+
|
|
3436
3552
|
/** @public */
|
|
3437
3553
|
export declare function ListenStep({ _stepIndex, _totalSteps, ...rest }: StepWrapperProps): JSX.Element;
|
|
3438
3554
|
|
|
@@ -3441,6 +3557,9 @@ export declare function ListPopup<T extends {
|
|
|
3441
3557
|
id?: string | number;
|
|
3442
3558
|
}>(props: PopupListProps<T>): JSX.Element;
|
|
3443
3559
|
|
|
3560
|
+
/** 첫 쪽으로 돌아갈 까닭을 잇는 값 — **글로 적을 수 있는 것**만 받는다(아래 `resetOn` 참고) @public */
|
|
3561
|
+
export declare type ListResetValue = string | number | boolean | null | undefined;
|
|
3562
|
+
|
|
3444
3563
|
/** @public */
|
|
3445
3564
|
export declare function Loader(props: LoaderProps): JSX.Element;
|
|
3446
3565
|
|
|
@@ -4526,6 +4645,18 @@ export declare interface PaymentPlanTier {
|
|
|
4526
4645
|
/** @public */
|
|
4527
4646
|
export declare const pctToLevel: (pct: number) => number;
|
|
4528
4647
|
|
|
4648
|
+
/**
|
|
4649
|
+
* **대표번호를 뺀 것만** 받는 자 — `PHONE_REGEX` 에서 대표번호 갈래를 뺀 것이다.
|
|
4650
|
+
*
|
|
4651
|
+
* 말귀 회원앱·어드민이 `isValidPhone(phone) && phone.startsWith('0')` 를 한 벌씩 들고
|
|
4652
|
+
* 있었다(2026-09-27). 그 칸들은 **사람에게 거는 번호**를 받는 자리라 `1588-1234` 를
|
|
4653
|
+
* 거절해 왔다 — 뜻은 맞는데 **`'0'` 으로 시작하는가**로 적혀 있었다. 그것이 지금 같은
|
|
4654
|
+
* 뜻인 것은 `PHONE_REGEX` 의 비-0 갈래가 대표번호 하나뿐이라서일 뿐이고, 0 으로 시작하지
|
|
4655
|
+
* 않는 대역이 하나 더 생기는 날 **두 앱이 조용히 그것을 받게 된다.** 뜻대로 적어 둔다.
|
|
4656
|
+
* @public
|
|
4657
|
+
*/
|
|
4658
|
+
export declare const PERSONAL_PHONE_REGEX: RegExp;
|
|
4659
|
+
|
|
4529
4660
|
/**
|
|
4530
4661
|
* 인물 소개 카드 — 원형 사진 위에 이름·직함, 아래에 경력 목록을 세로로 쌓는다.
|
|
4531
4662
|
* 회사 소개의 운영진 섹션, 연구진 목록 등에 사용.
|
|
@@ -4553,23 +4684,6 @@ export declare interface PersonCardProps {
|
|
|
4553
4684
|
className?: string;
|
|
4554
4685
|
}
|
|
4555
4686
|
|
|
4556
|
-
/**
|
|
4557
|
-
* 연락처가 **`formatPhone` 이 끝까지 채워 낸 모양** 인가.
|
|
4558
|
-
*
|
|
4559
|
-
* 만드는 쪽(`formatPhone`)은 코어인데 받는 쪽(형식 검사)은 소비자마다 한 벌씩 두고 있었다 —
|
|
4560
|
-
* 말귀 회원앱·어드민에 같은 정규식이 한 벌씩 있었고, 둘이 같은지는 짝 시험이 지켰지만
|
|
4561
|
-
* **코어가 만든 모양과 맞는지는 아무도 안 쟀다.** 실제로 코어가 050X·0303(앞자리 네 자리)을
|
|
4562
|
-
* 받았을 때 그 두 벌도 손으로 함께 고쳐야 했다. 그래서 짝을 여기 둔다 — 아래 시험이
|
|
4563
|
-
* "formatPhone 이 끝까지 채운 값은 늘 통과한다" 를 못 박는다.
|
|
4564
|
-
*
|
|
4565
|
-
* `formatPhone` 은 **덜 채운 입력을 그대로 돌려준다**(`010-12`). 그래서 빈값 검사만으로는
|
|
4566
|
-
* 모자라고, 이 함수가 그 자리를 맡는다.
|
|
4567
|
-
*
|
|
4568
|
-
* 받는 꼴 — 휴대폰·지역번호(02·0NN)·050X 안심번호·0303 착신전환·**1588 등 대표번호**.
|
|
4569
|
-
* 대표번호는 `formatPhone` 이 `1588-1234` 로 만들어 내므로 여기서도 받는다
|
|
4570
|
-
* (0 으로 시작하는 것만 받던 소비자 정규식은 그 번호를 거절했다 — 코어가 만든 모양을 코어가 거절하는 꼴).
|
|
4571
|
-
* @public
|
|
4572
|
-
*/
|
|
4573
4687
|
export declare const PHONE_REGEX: RegExp;
|
|
4574
4688
|
|
|
4575
4689
|
/**
|
|
@@ -7355,6 +7469,11 @@ export declare function tableTrends(records: readonly ScoreRecord[]): TableTrend
|
|
|
7355
7469
|
*/
|
|
7356
7470
|
export declare function Tabs({ items, value, onChange, className, size }: TabsProps): JSX.Element;
|
|
7357
7471
|
|
|
7472
|
+
/** 이 자가 보는 것은 **이것뿐이다** — 세션의 나머지는 알 필요가 없다 @public */
|
|
7473
|
+
export declare interface TabSessionLike {
|
|
7474
|
+
access_token: string;
|
|
7475
|
+
}
|
|
7476
|
+
|
|
7358
7477
|
/** @public */
|
|
7359
7478
|
export declare interface TabsProps {
|
|
7360
7479
|
items?: TabItem[];
|
|
@@ -9070,6 +9189,39 @@ export declare function useDeviceManagement(): {
|
|
|
9070
9189
|
handleSelectFromList: (form: DeviceForm) => void;
|
|
9071
9190
|
};
|
|
9072
9191
|
|
|
9192
|
+
/**
|
|
9193
|
+
* **쓰던 것을 버리고 닫으려 할 때 한 번 더 묻는다.**
|
|
9194
|
+
*
|
|
9195
|
+
* 코어 0.128.0 부터 창은 어두운 바탕을 눌러도 닫힌다(사장님 지시 2026-09-14 "기본값으로").
|
|
9196
|
+
* 긴 글을 받는 창에서 바깥을 한 번 잘못 누르면 쓰던 것이 통째로 사라진다 — 그 자리를 막는다.
|
|
9197
|
+
*
|
|
9198
|
+
* 말귀 회원앱과 어드민에 한 벌씩 있었고(2026-09-27) **어드민 쪽이 윗집합**이었다. 갈라져
|
|
9199
|
+
* 있던 탓에 어드민에서 찾은 고침(`allowClose`)이 회원앱으로 건너오지 못했다 — 회원앱
|
|
9200
|
+
* 고객 추가 창이 **저장을 끝내고도 "저장했는데 버릴까요?" 를 물었다.** 여기 한 벌로 둔다.
|
|
9201
|
+
*
|
|
9202
|
+
* **`allowClose` 가 그 고침이다.** 저장이 끝나고 창이 스스로 닫을 때(`closeWithAnimation`)도
|
|
9203
|
+
* 그 길은 `beforeClose` 를 거치는데, 폼은 방금 채워 달라진 채라 "버릴까요" 가 뜬다. 저장에
|
|
9204
|
+
* 성공한 직후 `allowClose()` 를 부르면 **다음 한 번만** 안 묻고 닫는다. 같은 틱에 닫으므로
|
|
9205
|
+
* 상태가 아니라 ref 로 든다 — 상태로 들면 다시 그려지기 전에 닫혀 안 읽힌다.
|
|
9206
|
+
*
|
|
9207
|
+
* **물음창은 JSX 어디에 두든 된다.** 창은 `document.body` 로 옮겨 붙고(`createPortal`)
|
|
9208
|
+
* 그 차례는 코드 자리가 아니라 **뜬 시각**으로 정해지므로, 나중에 뜨는 이 물음창이 늘 위다.
|
|
9209
|
+
*
|
|
9210
|
+
* 문구는 `labels` 로 바꾼다 — 안 주면 사전(`common.discardConfirm.*`)을 따르고, 사전이
|
|
9211
|
+
* 없으면 한국어 기본값으로 그린다(번역을 안 쓰는 소비자도 그대로 읽힌다).
|
|
9212
|
+
*
|
|
9213
|
+
* ```tsx
|
|
9214
|
+
* const { beforeClose, discardPopup, allowClose } = useDiscardGuard(달라졌나, 닫기)
|
|
9215
|
+
* const 저장 = async () => { await 보내기(); allowClose(); closeWithAnimation() }
|
|
9216
|
+
* return <Popup title={제목} beforeClose={beforeClose} …>{…}{discardPopup}</Popup>
|
|
9217
|
+
* ```
|
|
9218
|
+
*
|
|
9219
|
+
* @param isDirty 지금 닫으면 잃을 것이 있는가
|
|
9220
|
+
* @param onClose 버리기로 했을 때 실제로 닫는 일
|
|
9221
|
+
* @public
|
|
9222
|
+
*/
|
|
9223
|
+
export declare function useDiscardGuard(isDirty: boolean, onClose: () => void, labels?: DiscardGuardLabels): DiscardGuard;
|
|
9224
|
+
|
|
9073
9225
|
/**
|
|
9074
9226
|
* 현재 화면의 제목을 `document.title`에 반영한다 — `'<제목> · <앱 이름>'`.
|
|
9075
9227
|
*
|
|
@@ -9159,6 +9311,42 @@ export declare function useHoverAnchor(): {
|
|
|
9159
9311
|
/** @public */
|
|
9160
9312
|
export declare function useI18n(): I18nContextValue;
|
|
9161
9313
|
|
|
9314
|
+
/**
|
|
9315
|
+
* **목록 화면의 조작부 한 벌** — 찾는 말 · 쪽크기 · 쪽 번호, 그리고 **언제 첫 쪽으로 돌아가는가.**
|
|
9316
|
+
*
|
|
9317
|
+
* 화면 셋이 같은 다섯 줄을 손으로 엮고 있었다(어드민 문장 자료 · 어드민 낱말 자료 · 회원앱 낱말
|
|
9318
|
+
* 관리 — 2026-09-27). `usePagedList` 를 코어로 올린 까닭이 한 겹 위에서 되풀이된 자리다.
|
|
9319
|
+
*
|
|
9320
|
+
* **이 훅이 막는 것은 조용히 틀어지는 쪽이다.** `usePageResetOnChange` 의 열쇠에서 한 조각만
|
|
9321
|
+
* 빠뜨리면 — 이를테면 쪽크기 — 25에서 100으로 바꿔도 `page` 가 7에 머문다. 총 쪽수가 2로 줄어
|
|
9322
|
+
* `usePagedList` 가 마지막 쪽으로 조이니 **2쪽이 뜨는데 사람은 1쪽을 본다고 믿는다.** 오류도
|
|
9323
|
+
* 빈 화면도 아니고 **엉뚱한 쪽**이라 아무도 못 알아챈다. 그래서 검색어와 쪽크기는 부르는 쪽이
|
|
9324
|
+
* 적는 것이 아니라 **이 훅이 스스로 넣는다.** 적지 않은 것은 빠뜨릴 수도 없다.
|
|
9325
|
+
*
|
|
9326
|
+
* 쪽크기 기본값(`'25'`)도 여기가 쥔다 — 세 화면에 `Number(pageSize) || 25` 가 한 벌씩 있었다.
|
|
9327
|
+
* (코어 `Pagination` 에는 쪽크기 고르개가 없어 맞춰야 할 기본값이 따로 있지는 않다. 겹친 것은
|
|
9328
|
+
* 화면 셋의 `25` 뿐이다.)
|
|
9329
|
+
*
|
|
9330
|
+
* **거르고 세우는 일은 안 맡는다.** 세 화면의 잣대가 서로 다르고(`묶음에서찾기` ·
|
|
9331
|
+
* `searchAndRankWords` · 보기별 거르기 + 낱말 순위), 거르기는 화면이 아는 일이다. 무엇보다
|
|
9332
|
+
* 그 일을 이 훅 안에 넣으면 **부르는 쪽의 `useMemo` 가 사라진다** — 낱말 자료 화면은 23,000개를
|
|
9333
|
+
* 세우므로 매 그림마다 다시 세우게 된다.
|
|
9334
|
+
*
|
|
9335
|
+
* 그래서 두 걸음이다. 거르는 일이 가운데 있고, 그 자리가 곧 `useMemo` 가 설 자리다:
|
|
9336
|
+
*
|
|
9337
|
+
* ```tsx
|
|
9338
|
+
* const 목록 = useListControls({ resetOn: [보기] })
|
|
9339
|
+
* const 걸러낸것 = useMemo(() => 거르기(줄들, 목록.query, 보기), [줄들, 목록.query, 보기])
|
|
9340
|
+
* const { pageItems, totalPages, curPage } = usePagedList(걸러낸것, 목록.perPage, 목록.page)
|
|
9341
|
+
* ```
|
|
9342
|
+
*
|
|
9343
|
+
* 찾는 말을 이 훅이 쥐는 것은 **첫 쪽으로 돌아갈 까닭**이기 때문이다. 쥐고 있으니 열쇠에 넣는
|
|
9344
|
+
* 일도 여기서 끝난다 — `목록.query` 는 위처럼 거를 때 그대로 쓴다.
|
|
9345
|
+
*
|
|
9346
|
+
* @public
|
|
9347
|
+
*/
|
|
9348
|
+
export declare function useListControls(opts?: ListControlsOptions): ListControls;
|
|
9349
|
+
|
|
9162
9350
|
/** @public */
|
|
9163
9351
|
export declare function useMediaQuery(query: string): boolean;
|
|
9164
9352
|
|
|
@@ -74,6 +74,45 @@
|
|
|
74
74
|
background: rgba(255,255,255,0.25);
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
/* **막대는 옅은 흰색이다**(사장님 지시 2026-09-27) — 얹으면 짙어진다.
|
|
78
|
+
|
|
79
|
+
브랜드 색(`--color-main` → 얹으면 `--color-sub`)으로 한 번 가 봤다가 되돌렸다.
|
|
80
|
+
이 지면이 바로 그 두 색 사이를 지나가는 그라디언트라, **막대가 어딘가에서 지면과
|
|
81
|
+
같아진다** — 재 보니 main 은 레일 맨 위에서 1.06, sub 은 맨 아래에서 1.01 이었다.
|
|
82
|
+
게다가 둘이 엇갈려서 위쪽에서는 얹으면 잘 보이고 아래쪽에서는 얹으면 **덜** 보였다.
|
|
83
|
+
색을 고르는 것으로는 못 푸는 자리다 — 지면이 색을 갈아입으니까.
|
|
84
|
+
|
|
85
|
+
투명도로 푼다. 흰빛을 옅게 얹으면 지면이 무슨 색이든 **그만큼 밝아져** 어디서나
|
|
86
|
+
같은 만큼 떠오른다. 이 블록의 구분선(`rgba(255,255,255,0.25)`)이 쓰는 방식과 같다.
|
|
87
|
+
|
|
88
|
+
**재어 둔다 — 정하는 것은 사람이다**(RELEASE.md "색은 문을 두지 않는다").
|
|
89
|
+
220×518 레일에서 가장 나쁜 자리(지면이 가장 밝은 아래쪽)의 대비다:
|
|
90
|
+
|
|
91
|
+
alpha 0.25 0.4 0.5 0.6 0.75 0.9 1.0
|
|
92
|
+
대비 1.18 1.30 1.39 1.49 1.65 1.82 1.94
|
|
93
|
+
|
|
94
|
+
**흰빛으로는 3.0 에 못 간다** — 꽉 채운 흰색도 1.94 다. 아래쪽 지면(민트)이 이미 밝아서,
|
|
95
|
+
거기서 3.0 을 얻으려면 막대가 **어두워져야** 한다. 그러면 위쪽(파랑)에서 또 뒤집힌다.
|
|
96
|
+
지금 값(0.4 → 얹으면 0.75)은 그 안에서 고른 것이고, 브랜드 색과 달리 **어디서도 지면과
|
|
97
|
+
같아지지 않고 얹으면 늘 더 보인다**(가장 나쁜 자리에서도 1.30 → 1.65). */
|
|
98
|
+
.header--manager .side-nav__scroll-thumb {
|
|
99
|
+
background: rgba(255, 255, 255, 0.4);
|
|
100
|
+
}
|
|
101
|
+
.header--manager .side-nav__scroll-thumb:hover,
|
|
102
|
+
.header--manager .side-nav__scroll-thumb:active {
|
|
103
|
+
background: rgba(255, 255, 255, 0.75);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* **못 쓰는 항목은 회색이 아니라 옅은 흰색이다.** 기본 꼴의 `--color-lightGray` 는
|
|
107
|
+
흰 지면에서 물러나 보이라고 고른 색인데, 이 지면에서는 푸른 회색이 그라디언트 위에
|
|
108
|
+
떠서 **글자는 희고 아이콘만 탁한** 꼴이 됐다(아이콘 규칙이 0,3,0 이라 매니저 규칙
|
|
109
|
+
0,2,0 을 이긴다 — 사장님 지적 2026-09-27). 물러나는 일은 줄에 이미 걸린
|
|
110
|
+
`opacity: 0.6` 이 한다. */
|
|
111
|
+
.header--manager .side-nav__item.disabled,
|
|
112
|
+
.header--manager .side-nav__item.disabled .side-nav__icon {
|
|
113
|
+
color: var(--color-deepWhite);
|
|
114
|
+
}
|
|
115
|
+
|
|
77
116
|
/* 메뉴 아이템 기본 */
|
|
78
117
|
.header--manager .side-nav__item {
|
|
79
118
|
color: var(--color-deepWhite);
|
|
@@ -126,140 +165,139 @@
|
|
|
126
165
|
.header__bottom { display: none; }
|
|
127
166
|
}
|
|
128
167
|
|
|
129
|
-
/* Tablet:
|
|
168
|
+
/* Tablet: 72px 접힌 레일 — 마우스를 얹거나 초점이 들어가면 220px 으로 펼쳐진다.
|
|
169
|
+
|
|
170
|
+
**펼친 꼴은 넓은 화면과 같아야 한다**(사장님 지시 2026-09-27). 그래서 이 블록은
|
|
171
|
+
"접힌 꼴만 덮고, 펼치면 아무것도 안 덮는다" 로 적는다 — 덮은 것을 하나하나 되돌리는
|
|
172
|
+
대신 **애초에 안 덮는다.**
|
|
173
|
+
|
|
174
|
+
예전에는 되돌리는 쪽이었다: 접힌 꼴 규칙마다 `:hover` 짝을 하나씩 두어 되돌렸다.
|
|
175
|
+
그러면 **짝을 빠뜨린 것이 조용히 남는다** — 실제로 넓은 화면과 여섯 군데가 달랐다
|
|
176
|
+
(실측 2026-09-27): 목록 여백 11 → 8, 항목 사이 12 → 10, 모서리 10 → 12,
|
|
177
|
+
이름표 `max-width` none → 200, 그리고 **하위 항목의 들여쓰기 30 → 16**. 마지막 것이
|
|
178
|
+
눈에 띄는 자리다 — 되돌리는 규칙(`padding: 0 16px`)이 하위 항목 규칙(0,1,0)보다 세서
|
|
179
|
+
들여쓰기가 통째로 사라졌고, 그 짝은 애초에 적힌 적이 없다.
|
|
180
|
+
|
|
181
|
+
안 덮으면 되돌릴 것도 없다. 새 규칙이 넓은 화면에 생겨도 펼친 레일이 저절로 따라간다. */
|
|
130
182
|
@media (min-width: 768px) and (max-width: 1023px) {
|
|
131
183
|
.header {
|
|
132
184
|
width: 72px;
|
|
185
|
+
transition: width 180ms ease;
|
|
186
|
+
overflow: visible;
|
|
187
|
+
}
|
|
188
|
+
.header:hover,
|
|
189
|
+
.header:focus-within {
|
|
190
|
+
width: 220px; /* match desktop */
|
|
191
|
+
}
|
|
192
|
+
/* JS 가 접힌 채로 못 박으면(예: 화면을 옮긴 뒤) 호버해도 안 펼쳐진다 */
|
|
193
|
+
.header.header--force-collapsed,
|
|
194
|
+
.header.header--force-collapsed:hover,
|
|
195
|
+
.header.header--force-collapsed:focus-within {
|
|
196
|
+
width: 72px;
|
|
133
197
|
}
|
|
198
|
+
|
|
134
199
|
.header__branding { height: 80px; }
|
|
135
|
-
|
|
200
|
+
|
|
201
|
+
/* ── 두 꼴에 모두 — 움직임과 스크롤바 ─────────────────────────────────────
|
|
202
|
+
**접힌 레일에서는 스크롤바 자리를 비우지 않는다.** 72px 에서 양쪽 gutter(클래식
|
|
203
|
+
11px × 2)를 떼면 항목이 28px 로 짓눌려 24px 아이콘이 10px 삐져나간다(실측).
|
|
204
|
+
아이콘만 있는 좁은 레일에는 막대를 그릴 자리가 애초에 없다 — 막대는 숨기고
|
|
205
|
+
휠·터치·키보드 스크롤은 그대로 둔다. 넓은 화면도 막대를 숨기므로(제 막대를
|
|
206
|
+
따로 그린다) 이 줄은 두 꼴에 같다. */
|
|
136
207
|
.header .side-nav__menu {
|
|
137
|
-
padding: 0 var(--spacing-xs);
|
|
138
208
|
transition: padding 180ms ease;
|
|
139
|
-
/* **접힌 레일에서는 스크롤바 자리를 비우지 않는다.**
|
|
140
|
-
72px 에서 양쪽 gutter(클래식 11px × 2)를 떼면 항목이 28px 로 짓눌려 24px
|
|
141
|
-
아이콘이 10px 삐져나간다(실측). 아이콘만 있는 좁은 레일에는 스크롤바를 그릴
|
|
142
|
-
자리가 애초에 없다 — 막대는 숨기고 휠·터치·키보드 스크롤은 그대로 둔다.
|
|
143
|
-
좌우는 padding 만으로 같다(실측 8px 기준: 좌 9 / 우 9, 항목 52). */
|
|
144
209
|
scrollbar-gutter: auto;
|
|
145
210
|
scrollbar-width: none;
|
|
146
211
|
}
|
|
147
212
|
.header .side-nav__menu::-webkit-scrollbar { display: none; }
|
|
148
213
|
.header .side-nav__item {
|
|
149
|
-
/* (52px item - 24px icon) / 2 = 14px to center icon when collapsed */
|
|
150
|
-
padding: 0 14px;
|
|
151
|
-
gap: 0;
|
|
152
|
-
border-radius: var(--radius-card);
|
|
153
|
-
/* **단추를 52 로 못 박고 가운데 세운다**(사장님 지시 2026-09-25). 여백만 8px 으로 낮추면
|
|
154
|
-
항목이 자리를 다 먹어 54 가 되고, 위 14px 셈이 어긋나 아이콘이 1px 오른쪽으로 밀린다.
|
|
155
|
-
52 로 두면 그 셈이 그대로 맞아 아이콘이 14 / 14 로 선다. */
|
|
156
|
-
width: 52px;
|
|
157
|
-
margin: 0 auto;
|
|
158
214
|
transition: padding 180ms ease, gap 180ms ease, width 180ms ease;
|
|
159
215
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/* **짧은 구분선도 가운데로.** 그 선은 `margin: 4px var(--spacing-m)` 로 서는데, 명시 폭
|
|
170
|
-
(`--spacing-xl`, 24px)을 가진 플렉스 항목이라 `stretch` 가 안 먹고 **시작 모서리**로
|
|
171
|
-
떨어진다. 그래서 52px 목록 안에서 좌 16 자리에 서고 중심이 38 이 된다 — 레일 중심 36,
|
|
172
|
-
항목·아이콘 중심 36 인데 그 선만 **2px 오른쪽**이었다(실측 2026-09-26, 사장님이
|
|
173
|
-
"이 선이 태블릿일 때 가운데가 아니네" 로 짚으신 것이 이것이다). 가로도 넘친다 —
|
|
174
|
-
16 + 24 + 16 = 56 인데 목록은 52 라 오른쪽 여백 4px 이 잘린다.
|
|
175
|
-
`auto` 여백은 플렉스 항목을 남는 자리 가운데로 민다. 폭·높이·색은 그대로다. */
|
|
176
|
-
.header .side-nav__separator--short {
|
|
177
|
-
margin-left: auto;
|
|
178
|
-
margin-right: auto;
|
|
216
|
+
/* 이름표는 **접으면 자리까지 없앤다**(`max-width: 0`) — `visibility` 로만 숨기면
|
|
217
|
+
그만큼 자리를 먹어 아이콘이 가운데를 벗어난다. 그 값은 아래 접힌 꼴에 있다. */
|
|
218
|
+
.header .side-nav__label {
|
|
219
|
+
display: inline-block;
|
|
220
|
+
overflow: hidden;
|
|
221
|
+
white-space: nowrap;
|
|
222
|
+
transition: max-width 180ms ease, opacity 180ms ease, transform 180ms ease;
|
|
179
223
|
}
|
|
180
224
|
|
|
181
|
-
|
|
182
|
-
|
|
225
|
+
/* ── 접힌 꼴만 ────────────────────────────────────────────────────────────
|
|
226
|
+
펼치면(`:hover`·`:focus-within`) 이 아래가 통째로 안 걸리고 넓은 화면 규칙이 그대로
|
|
227
|
+
산다. 못 박아 접어 둔 레일은 호버해도 안 펼쳐지므로 늘 걸린다 — 그래서 선택자가
|
|
228
|
+
둘씩이다. */
|
|
229
|
+
.header:not(:hover):not(:focus-within) .side-nav__menu,
|
|
230
|
+
.header--force-collapsed .side-nav__menu {
|
|
231
|
+
/* 좌우는 여백만으로 같다(실측 8px 기준: 좌 9 / 우 9, 항목 52) */
|
|
183
232
|
padding: 0 var(--spacing-xs);
|
|
184
233
|
}
|
|
185
234
|
|
|
186
|
-
.header:hover .side-nav__item,
|
|
187
|
-
.header
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
235
|
+
.header:not(:hover):not(:focus-within) .side-nav__item,
|
|
236
|
+
.header--force-collapsed .side-nav__item {
|
|
237
|
+
/* (52px 항목 - 24px 아이콘) / 2 = 14px — 접힌 꼴에서 아이콘을 가운데 세운다 */
|
|
238
|
+
padding: 0 14px;
|
|
239
|
+
gap: 0;
|
|
240
|
+
border-radius: var(--radius-card);
|
|
241
|
+
/* **단추를 52 로 못 박고 가운데 세운다**(사장님 지시 2026-09-25). 여백만 8px 으로 낮추면
|
|
242
|
+
항목이 자리를 다 먹어 54 가 되고, 위 14px 셈이 어긋나 아이콘이 1px 오른쪽으로 밀린다. */
|
|
243
|
+
width: 52px;
|
|
244
|
+
margin: 0 auto;
|
|
245
|
+
/* 아래 배지의 기준 */
|
|
246
|
+
position: relative;
|
|
194
247
|
}
|
|
195
248
|
|
|
196
|
-
/*
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}
|
|
249
|
+
/* **배지는 단추 오른쪽 위로.** 인라인 배지는 이름표 뒤에 서는데, 접히면 이름표가 폭 0 이라
|
|
250
|
+
배지가 아이콘 바로 옆에 붙는다 — 단추(52)를 10px 넘어서고 통이 그 자리를 잘라, 동그라미가
|
|
251
|
+
반쯤 베인 채로 남았다(실측 2026-09-27: 단추 344~396, 배지 382~406, 통 안쪽 끝 397.
|
|
252
|
+
사장님이 짚으신 그림이 이것이다).
|
|
201
253
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
.header:hover .side-nav__separator--short,
|
|
206
|
-
.header:focus-within .side-nav__separator--short {
|
|
207
|
-
margin-left: var(--spacing-m);
|
|
208
|
-
margin-right: var(--spacing-m);
|
|
209
|
-
}
|
|
254
|
+
**모서리 값이 0 인 것은 `--top-right` 기본값(-6px)과 다르다** — BottomNav 와 같은 까닭이다.
|
|
255
|
+
통이 넘침을 잘라서(넘침 셈에 그 `hidden` 이 필요하다) 바깥으로 나간 6px 은 그대로 사라진다.
|
|
256
|
+
안쪽 모서리에 얹으면 52 안에 온전히 들어온다(28~52).
|
|
210
257
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
white-space: nowrap;
|
|
220
|
-
pointer-events: none;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
.header {
|
|
224
|
-
width: 72px;
|
|
225
|
-
transition: width 180ms ease;
|
|
226
|
-
overflow: visible;
|
|
258
|
+
인라인 배지는 흰 테두리를 빼 두는데(흐름 안에 서니 가를 것이 없다), 아이콘 위에 얹히면
|
|
259
|
+
그 테두리가 있어야 갈린다 — 24px 배지의 테두리는 1px 이다. */
|
|
260
|
+
.header:not(:hover):not(:focus-within) .side-nav__item > .circle-badge,
|
|
261
|
+
.header--force-collapsed .side-nav__item > .circle-badge {
|
|
262
|
+
position: absolute;
|
|
263
|
+
top: 0;
|
|
264
|
+
right: 0;
|
|
265
|
+
border: 1px solid var(--color-pureWhite);
|
|
227
266
|
}
|
|
228
267
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
268
|
+
/* **발치도 가운데로.** 발치 줄은 좌우 여백이 없어 단추 40px 이 왼쪽에 붙었고, 레일
|
|
269
|
+
가운데(37)보다 5px 왼쪽에 서서 위 아이콘들과 눈에 띄게 어긋났다(말귀 실측 2026-09-25). */
|
|
270
|
+
.header:not(:hover):not(:focus-within) .side-nav__footer,
|
|
271
|
+
.header--force-collapsed .side-nav__footer {
|
|
272
|
+
align-items: center;
|
|
232
273
|
}
|
|
233
274
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
275
|
+
/* **짧은 구분선도 가운데로.** 그 선은 `margin: 4px var(--spacing-m)` 로 서는데, 명시 폭
|
|
276
|
+
(24px)을 가진 플렉스 항목이라 `stretch` 가 안 먹고 **시작 모서리**로 떨어진다. 그래서
|
|
277
|
+
52px 목록 안에서 중심이 38 이 됐다 — 레일·항목·아이콘 중심은 36 인데 그 선만 2px
|
|
278
|
+
오른쪽이었다(실측 2026-09-26). `auto` 여백이 남는 자리 가운데로 민다.
|
|
279
|
+
펼치면 이 규칙이 안 걸리고 16px 으로 돌아가는데, 그 16 은 눈대중이 아니라 **아이콘
|
|
280
|
+
왼쪽 모서리와 줄을 맞추는 값**이다(220px 레일 실측: 목록 482 · 아이콘 498 · 선 498). */
|
|
281
|
+
.header:not(:hover):not(:focus-within) .side-nav__separator--short,
|
|
282
|
+
.header--force-collapsed .side-nav__separator--short {
|
|
283
|
+
margin-left: auto;
|
|
284
|
+
margin-right: auto;
|
|
240
285
|
}
|
|
241
286
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
이미 높거나 동일 특이도에서 소스 순서상 뒤에 있어 자연스럽게 우선 적용됨. */
|
|
245
|
-
.header.header--force-collapsed {
|
|
246
|
-
width: 72px;
|
|
247
|
-
}
|
|
248
|
-
.header.header--force-collapsed .side-nav__label {
|
|
287
|
+
.header:not(:hover):not(:focus-within) .side-nav__label,
|
|
288
|
+
.header--force-collapsed .side-nav__label {
|
|
249
289
|
max-width: 0;
|
|
250
290
|
opacity: 0;
|
|
251
291
|
transform: translateX(-6px);
|
|
252
292
|
pointer-events: none;
|
|
253
293
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
.header
|
|
259
|
-
.header
|
|
260
|
-
|
|
261
|
-
margin-right: auto;
|
|
262
|
-
}
|
|
294
|
+
|
|
295
|
+
/* 접힌 72px 레일에는 로고가 설 자리가 없다 — 심볼로 선다. 펼치면 넓은 화면과 같은
|
|
296
|
+
얼굴이 되어야 하므로(같은 220px 폭이다) 여기서만 바꾼다. */
|
|
297
|
+
.header:not(:hover):not(:focus-within) .brand-identity__image--symbol,
|
|
298
|
+
.header--force-collapsed .brand-identity__image--symbol { display: block; }
|
|
299
|
+
.header:not(:hover):not(:focus-within) .brand-identity__image--logo,
|
|
300
|
+
.header--force-collapsed .brand-identity__image--logo { display: none; }
|
|
263
301
|
}
|
|
264
302
|
|
|
265
303
|
/* Mobile: top header + bottom bar */
|
|
@@ -316,24 +354,7 @@
|
|
|
316
354
|
|
|
317
355
|
@media (min-width: 768px) and (max-width: 1023px) {
|
|
318
356
|
.brand-identity__root { height: 100px; }
|
|
319
|
-
|
|
320
|
-
/* 접힌 72px 레일에는 로고가 설 자리가 없다 — 심볼로 선다. */
|
|
321
|
-
.brand-identity__image--symbol { display: block; }
|
|
322
|
-
.brand-identity__image--logo { display: none; }
|
|
323
|
-
|
|
324
|
-
/* **펼치면 넓은 화면과 같은 얼굴이 된다.** 220px 으로 펼친 레일은 넓은 화면과 같은 폭이라,
|
|
325
|
-
거기서 심볼로 남으면 같은 레일이 같은 폭에서 다른 얼굴을 한다. */
|
|
326
|
-
.header:hover .brand-identity__image--symbol,
|
|
327
|
-
.header:focus-within .brand-identity__image--symbol { display: none; }
|
|
328
|
-
.header:hover .brand-identity__image--logo,
|
|
329
|
-
.header:focus-within .brand-identity__image--logo { display: block; }
|
|
330
|
-
|
|
331
|
-
/* **못 박아 접어 둔 레일은 호버해도 안 펼쳐진다**(`:hover { width: 72px }`) — 그때는 심볼 그대로다.
|
|
332
|
-
이 갈래가 없으면 72px 자리에 로고가 들어가 잘린다. */
|
|
333
|
-
.header.header--force-collapsed:hover .brand-identity__image--symbol,
|
|
334
|
-
.header.header--force-collapsed:focus-within .brand-identity__image--symbol { display: block; }
|
|
335
|
-
.header.header--force-collapsed:hover .brand-identity__image--logo,
|
|
336
|
-
.header.header--force-collapsed:focus-within .brand-identity__image--logo { display: none; }
|
|
357
|
+
/* 심볼이냐 로고냐는 **접혔느냐**로 갈린다 — 그 갈래는 위 레일 블록에 있다(한자리에 모아 둔다). */
|
|
337
358
|
}
|
|
338
359
|
|
|
339
360
|
@media (max-width: 767px) {
|