mhkim-node #412
|
|
@ -0,0 +1,122 @@
|
||||||
|
# [계획서] 카테고리 드롭다운 - 3단계 깊이 구분 표시
|
||||||
|
|
||||||
|
> 관련 문서: [맥락노트](./CTI[맥락]-카테고리-깊이구분.md) | [체크리스트](./CTI[체크]-카테고리-깊이구분.md)
|
||||||
|
>
|
||||||
|
> 상태: **완료** (2026-03-11)
|
||||||
|
|
||||||
|
## 개요
|
||||||
|
|
||||||
|
카테고리 타입(`source="category"`) 드롭다운에서 3단계 계층(대분류 > 중분류 > 소분류)의 들여쓰기가 시각적으로 구분되지 않는 문제를 수정합니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 변경 전 동작
|
||||||
|
|
||||||
|
- `category_values` 테이블은 `parent_value_id`, `depth` 컬럼으로 3단계 계층 구조를 지원
|
||||||
|
- 백엔드 `buildHierarchy()`가 트리 구조를 정상적으로 반환
|
||||||
|
- 프론트엔드 `flattenTree()`가 트리를 평탄화하면서 **일반 ASCII 공백(`" "`)** 으로 들여쓰기 생성
|
||||||
|
- HTML이 연속 공백을 하나로 축소(collapse)하여 depth 1과 depth 2가 동일하게 렌더링됨
|
||||||
|
|
||||||
|
### 변경 전 코드 (flattenTree)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const prefix = depth > 0 ? " ".repeat(depth) + "└ " : "";
|
||||||
|
```
|
||||||
|
|
||||||
|
### 변경 전 렌더링 결과
|
||||||
|
|
||||||
|
```
|
||||||
|
신예철
|
||||||
|
└ 신2
|
||||||
|
└ 신22 ← depth 2인데 depth 1과 구분 불가
|
||||||
|
└ 신3
|
||||||
|
└ 신4
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 변경 후 동작
|
||||||
|
|
||||||
|
### 일반 공백을 Non-Breaking Space(`\u00A0`)로 교체
|
||||||
|
|
||||||
|
- `\u00A0`는 HTML에서 축소되지 않으므로 depth별 들여쓰기가 정확히 유지됨
|
||||||
|
- depth당 3칸(`\u00A0\u00A0\u00A0`)으로 시각적 계층 구분을 명확히 함
|
||||||
|
- 백엔드 변경 없음 (트리 구조는 이미 정상)
|
||||||
|
|
||||||
|
### 변경 후 코드 (flattenTree)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const prefix = depth > 0 ? "\u00A0\u00A0\u00A0".repeat(depth) + "└ " : "";
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 시각적 예시
|
||||||
|
|
||||||
|
| depth | prefix | 드롭다운 표시 |
|
||||||
|
|-------|--------|-------------|
|
||||||
|
| 0 (대분류) | `""` | `신예철` |
|
||||||
|
| 1 (중분류) | `"\u00A0\u00A0\u00A0└ "` | `···└ 신2` |
|
||||||
|
| 2 (소분류) | `"\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0└ "` | `······└ 신22` |
|
||||||
|
|
||||||
|
### 변경 전후 비교
|
||||||
|
|
||||||
|
```
|
||||||
|
변경 전: 변경 후:
|
||||||
|
신예철 신예철
|
||||||
|
└ 신2 └ 신2
|
||||||
|
└ 신22 ← 구분 불가 └ 신22 ← 명확히 구분
|
||||||
|
└ 신3 └ 신3
|
||||||
|
└ 신4 └ 신4
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 아키텍처
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A[category_values 테이블] -->|parent_value_id, depth| B[백엔드 buildHierarchy]
|
||||||
|
B -->|트리 JSON 응답| C[프론트엔드 API 호출]
|
||||||
|
C --> D[flattenTree 함수]
|
||||||
|
D -->|"depth별 \u00A0 prefix 생성"| E[SelectOption 배열]
|
||||||
|
E --> F{렌더링 모드}
|
||||||
|
F -->|비검색| G[SelectItem - label 표시]
|
||||||
|
F -->|검색| H[CommandItem - displayLabel 표시]
|
||||||
|
|
||||||
|
style D fill:#f96,stroke:#333,color:#000
|
||||||
|
```
|
||||||
|
|
||||||
|
**변경 지점**: `flattenTree` 함수 내 prefix 생성 로직 (주황색 표시)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 변경 대상 파일
|
||||||
|
|
||||||
|
| 파일 경로 | 변경 내용 | 변경 규모 |
|
||||||
|
|-----------|----------|----------|
|
||||||
|
| `frontend/components/v2/V2Select.tsx` (904행) | `flattenTree` prefix를 `\u00A0` 기반으로 변경 | 1줄 |
|
||||||
|
| `frontend/components/unified/UnifiedSelect.tsx` (632행) | 동일한 `flattenTree` prefix 변경 | 1줄 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 영향받는 기존 로직
|
||||||
|
|
||||||
|
V2Select.tsx의 `resolvedValue`(979행)에서 prefix를 제거하는 정규식:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const cleanLabel = o.label.replace(/^[\s└]+/, "").trim();
|
||||||
|
```
|
||||||
|
|
||||||
|
- JavaScript `\s`는 `\u00A0`를 포함하므로 기존 정규식이 정상 동작함
|
||||||
|
- 추가 수정 불필요
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 설계 원칙
|
||||||
|
|
||||||
|
- 백엔드 변경 없이 프론트엔드 표시 로직만 수정
|
||||||
|
- `flattenTree` 공통 함수 수정이므로 카테고리 타입 드롭다운 전체에 자동 적용
|
||||||
|
- DB 저장값(`valueCode`)에는 영향 없음 — `label`만 변경
|
||||||
|
- 기존 prefix strip 정규식(`/^[\s└]+/`)과 호환 유지
|
||||||
|
- `V2Select`와 `UnifiedSelect` 두 곳의 동일 패턴을 일관되게 수정
|
||||||
|
|
@ -0,0 +1,105 @@
|
||||||
|
# [맥락노트] 카테고리 드롭다운 - 3단계 깊이 구분 표시
|
||||||
|
|
||||||
|
> 관련 문서: [계획서](./CTI[계획]-카테고리-깊이구분.md) | [체크리스트](./CTI[체크]-카테고리-깊이구분.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 왜 이 작업을 하는가
|
||||||
|
|
||||||
|
- 품목정보 등록 모달의 "재고단위" 등 카테고리 드롭다운에서 3단계 계층이 시각적으로 구분되지 않음
|
||||||
|
- 예: "신22"가 "신2"의 하위인데, "신3", "신4"와 같은 레벨로 보임
|
||||||
|
- 사용자가 대분류/중분류/소분류 관계를 파악할 수 없어 잘못된 항목을 선택할 위험
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 핵심 결정 사항과 근거
|
||||||
|
|
||||||
|
### 1. 원인: HTML 공백 축소(collapse)
|
||||||
|
|
||||||
|
- **현상**: `flattenTree`에서 `" ".repeat(depth)`로 들여쓰기를 만들지만, HTML이 연속 공백을 하나로 합침
|
||||||
|
- **결과**: depth 1(`" └ "`)과 depth 2(`" └ "`)가 동일하게 렌더링됨
|
||||||
|
- **확인**: `SelectItem`, `CommandItem` 모두 `white-space: pre` 미적용 상태
|
||||||
|
|
||||||
|
### 2. 해결: Non-Breaking Space(`\u00A0`) 사용
|
||||||
|
|
||||||
|
- **결정**: 일반 공백 `" "`를 `"\u00A0"`로 교체
|
||||||
|
- **근거**: `\u00A0`는 HTML에서 축소되지 않아 depth별 들여쓰기가 정확히 유지됨
|
||||||
|
- **대안 검토**:
|
||||||
|
- `white-space: pre` CSS 적용 → 기각 (SelectItem, CommandItem 양쪽 모두 수정 필요, shadcn 기본 스타일 오버라이드 부담)
|
||||||
|
- CSS `padding-left` 사용 → 기각 (label 문자열 기반 옵션 구조에서 개별 아이템에 스타일 전달 어려움)
|
||||||
|
- 트리 문자(`│`, `├`, `└`) 조합 → 기각 (과도한 시각 정보, 단순 들여쓰기면 충분)
|
||||||
|
|
||||||
|
### 3. depth당 3칸 `\u00A0`
|
||||||
|
|
||||||
|
- **결정**: `"\u00A0\u00A0\u00A0".repeat(depth)` (depth당 3칸)
|
||||||
|
- **근거**: 기존 2칸은 `\u00A0`로 바꿔도 depth간 차이가 작음. 3칸이 시각적 구분에 적절
|
||||||
|
|
||||||
|
### 4. 두 파일 동시 수정
|
||||||
|
|
||||||
|
- **결정**: `V2Select.tsx`와 `UnifiedSelect.tsx` 모두 수정
|
||||||
|
- **근거**: 동일한 `flattenTree` 패턴이 두 컴포넌트에 존재. 하나만 수정하면 불일치 발생
|
||||||
|
|
||||||
|
### 5. 기존 prefix strip 정규식 호환
|
||||||
|
|
||||||
|
- **확인**: V2Select.tsx 979행의 `o.label.replace(/^[\s└]+/, "").trim()`
|
||||||
|
- **근거**: JavaScript `\s`는 `\u00A0`를 포함하므로 추가 수정 불필요
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 구현 중 발견한 사항
|
||||||
|
|
||||||
|
### CAT_ vs CATEGORY_ 접두사 불일치
|
||||||
|
|
||||||
|
테스트 과정에서 최고 관리자 계정으로 리스트 조회 시 `CAT_MMLL6U02_QH2V` 같은 코드가 그대로 표시되는 현상 발견.
|
||||||
|
|
||||||
|
- **원인**: 카테고리 값 생성 함수가 두 곳에 존재하며 접두사가 다름
|
||||||
|
- `CategoryValueAddDialog.tsx`: `CATEGORY_` 접두사
|
||||||
|
- `CategoryValueManagerTree.tsx`: `CAT_` 접두사
|
||||||
|
- **영향**: 리스트 해석 로직(`V2Repeater`, `InteractiveDataTable`, `UnifiedRepeater`)이 `CATEGORY_` 접두사만 인식하여 `CAT_` 코드는 라벨 변환 실패
|
||||||
|
- **판단**: 일반 회사 계정에서는 정상 동작 확인. 본 작업(들여쓰기 표시) 범위 외로 별도 이슈로 분리
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 관련 파일 위치
|
||||||
|
|
||||||
|
| 구분 | 파일 경로 | 설명 |
|
||||||
|
|------|----------|------|
|
||||||
|
| 수정 완료 | `frontend/components/v2/V2Select.tsx` | flattenTree 함수 (904행) |
|
||||||
|
| 수정 완료 | `frontend/components/unified/UnifiedSelect.tsx` | flattenTree 함수 (632행) |
|
||||||
|
| 백엔드 (변경 없음) | `backend-node/src/services/tableCategoryValueService.ts` | buildHierarchy 메서드 |
|
||||||
|
| UI 컴포넌트 (변경 없음) | `frontend/components/ui/select.tsx` | SelectItem 렌더링 |
|
||||||
|
| UI 컴포넌트 (변경 없음) | `frontend/components/ui/command.tsx` | CommandItem 렌더링 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 기술 참고
|
||||||
|
|
||||||
|
### flattenTree 동작 흐름
|
||||||
|
|
||||||
|
```
|
||||||
|
백엔드 API 응답 (트리 구조):
|
||||||
|
{
|
||||||
|
valueCode: "CAT_001", valueLabel: "신예철", children: [
|
||||||
|
{ valueCode: "CAT_002", valueLabel: "신2", children: [
|
||||||
|
{ valueCode: "CAT_003", valueLabel: "신22", children: [] }
|
||||||
|
]},
|
||||||
|
{ valueCode: "CAT_004", valueLabel: "신3", children: [] },
|
||||||
|
{ valueCode: "CAT_005", valueLabel: "신4", children: [] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
→ flattenTree 변환 후 (SelectOption 배열):
|
||||||
|
[
|
||||||
|
{ value: "CAT_001", label: "신예철" },
|
||||||
|
{ value: "CAT_002", label: "\u00A0\u00A0\u00A0└ 신2" },
|
||||||
|
{ value: "CAT_003", label: "\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0└ 신22" },
|
||||||
|
{ value: "CAT_004", label: "\u00A0\u00A0\u00A0└ 신3" },
|
||||||
|
{ value: "CAT_005", label: "\u00A0\u00A0\u00A0└ 신4" }
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### value vs label 분리
|
||||||
|
|
||||||
|
- `value` (저장값): `valueCode` — DB에 저장되는 값, 들여쓰기 없음
|
||||||
|
- `label` (표시값): prefix + `valueLabel` — 화면에만 보이는 값, 들여쓰기 포함
|
||||||
|
- 데이터 무결성에 영향 없음
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
# [체크리스트] 카테고리 드롭다운 - 3단계 깊이 구분 표시
|
||||||
|
|
||||||
|
> 관련 문서: [계획서](./CTI[계획]-카테고리-깊이구분.md) | [맥락노트](./CTI[맥락]-카테고리-깊이구분.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 공정 상태
|
||||||
|
|
||||||
|
- 전체 진행률: **100%** (완료)
|
||||||
|
- 현재 단계: 전체 완료
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 구현 체크리스트
|
||||||
|
|
||||||
|
### 1단계: 코드 수정
|
||||||
|
|
||||||
|
- [x] `V2Select.tsx` 904행 — `flattenTree` prefix를 `\u00A0` 기반으로 변경
|
||||||
|
- [x] `UnifiedSelect.tsx` 632행 — 동일한 `flattenTree` prefix 변경
|
||||||
|
|
||||||
|
### 2단계: 검증
|
||||||
|
|
||||||
|
- [x] depth 1 항목: 3칸 들여쓰기 + `└` 표시 확인
|
||||||
|
- [x] depth 2 항목: 6칸 들여쓰기 + `└` 표시, depth 1과 명확히 구분됨 확인
|
||||||
|
- [x] depth 0 항목: 들여쓰기 없이 원래대로 표시 확인
|
||||||
|
- [x] 항목 선택 후 값이 정상 저장되는지 확인 (valueCode 기준)
|
||||||
|
- [x] 기존 prefix strip 로직 정상 동작 확인 — JS `\s`가 `\u00A0` 포함하므로 호환
|
||||||
|
- [x] 검색 가능 모드(Combobox): 정상 동작 확인
|
||||||
|
- [x] 비검색 모드(Select): 렌더링 정상 확인
|
||||||
|
|
||||||
|
### 3단계: 정리
|
||||||
|
|
||||||
|
- [x] 린트 에러 없음 확인 (기존 에러 제외)
|
||||||
|
- [x] 계맥체 문서 최신화
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 참고: 최고 관리자 계정 표시 이슈
|
||||||
|
|
||||||
|
- 최고 관리자(`company_code = "*"`)로 리스트 조회 시 `CAT_MMLL6U02_QH2V` 같은 코드값이 그대로 노출되는 현상 발견
|
||||||
|
- 원인: `CategoryValueManagerTree.tsx`의 `generateCode()`가 `CAT_` 접두사를 사용하나, 리스트 해석 로직은 `CATEGORY_` 접두사만 인식
|
||||||
|
- 일반 회사 계정에서는 정상 표시됨을 확인
|
||||||
|
- 본 작업 범위 외로 판단하여 별도 이슈로 분리
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 변경 이력
|
||||||
|
|
||||||
|
| 날짜 | 내용 |
|
||||||
|
|------|------|
|
||||||
|
| 2026-03-11 | 계획서, 맥락노트, 체크리스트 작성 |
|
||||||
|
| 2026-03-11 | 1단계 코드 수정 완료 (V2Select.tsx, UnifiedSelect.tsx) |
|
||||||
|
| 2026-03-11 | 2단계 검증 완료, 3단계 문서 정리 완료 |
|
||||||
|
|
@ -458,6 +458,14 @@ select {
|
||||||
border-color: hsl(var(--destructive)) !important;
|
border-color: hsl(var(--destructive)) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* 채번 세그먼트 포커스 스타일 (shadcn Input과 동일한 3단 구조) */
|
||||||
|
.numbering-segment:focus-within {
|
||||||
|
box-shadow: 0 0 0 3px hsl(var(--ring) / 0.5);
|
||||||
|
outline: 2px solid hsl(var(--ring));
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
/* 필수 입력 경고 문구 (입력 필드 아래, 레이아웃 영향 없음) */
|
/* 필수 입력 경고 문구 (입력 필드 아래, 레이아웃 영향 없음) */
|
||||||
.validation-error-msg-wrapper {
|
.validation-error-msg-wrapper {
|
||||||
height: 0;
|
height: 0;
|
||||||
|
|
|
||||||
|
|
@ -629,7 +629,7 @@ export const UnifiedSelect = forwardRef<HTMLDivElement, UnifiedSelectProps>((pro
|
||||||
): SelectOption[] => {
|
): SelectOption[] => {
|
||||||
const result: SelectOption[] = [];
|
const result: SelectOption[] = [];
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
const prefix = depth > 0 ? " ".repeat(depth) + "└ " : "";
|
const prefix = depth > 0 ? "\u00A0\u00A0\u00A0".repeat(depth) + "└ " : "";
|
||||||
result.push({
|
result.push({
|
||||||
value: String(item.valueId), // valueId를 value로 사용 (채번 매핑과 일치)
|
value: String(item.valueId), // valueId를 value로 사용 (채번 매핑과 일치)
|
||||||
label: prefix + item.valueLabel,
|
label: prefix + item.valueLabel,
|
||||||
|
|
|
||||||
|
|
@ -909,10 +909,10 @@ export const V2Input = forwardRef<HTMLDivElement, V2InputProps>((props, ref) =>
|
||||||
const templateSuffix = templateParts.length > 1 ? templateParts.slice(1).join("") : "";
|
const templateSuffix = templateParts.length > 1 ? templateParts.slice(1).join("") : "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full items-center rounded-md border">
|
<div className="numbering-segment border-input flex h-full items-center rounded-md border outline-none transition-[color,box-shadow]">
|
||||||
{/* 고정 접두어 */}
|
{/* 고정 접두어 */}
|
||||||
{templatePrefix && (
|
{templatePrefix && (
|
||||||
<span className="text-muted-foreground bg-muted flex h-full items-center px-2 text-sm">
|
<span className="text-muted-foreground bg-muted flex h-full items-center rounded-l-[5px] px-2 text-sm">
|
||||||
{templatePrefix}
|
{templatePrefix}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
@ -945,13 +945,13 @@ export const V2Input = forwardRef<HTMLDivElement, V2InputProps>((props, ref) =>
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
placeholder="입력"
|
placeholder="입력"
|
||||||
className="h-full min-w-[60px] flex-1 bg-transparent px-2 text-sm focus-visible:outline-none"
|
className="h-full min-w-[60px] flex-1 border-0 bg-transparent px-2 text-sm ring-0"
|
||||||
disabled={disabled || isGeneratingNumbering}
|
disabled={disabled || isGeneratingNumbering}
|
||||||
style={inputTextStyle}
|
style={{ ...inputTextStyle, outline: 'none' }}
|
||||||
/>
|
/>
|
||||||
{/* 고정 접미어 */}
|
{/* 고정 접미어 */}
|
||||||
{templateSuffix && (
|
{templateSuffix && (
|
||||||
<span className="text-muted-foreground bg-muted flex h-full items-center px-2 text-sm">
|
<span className="text-muted-foreground bg-muted flex h-full items-center rounded-r-[5px] px-2 text-sm">
|
||||||
{templateSuffix}
|
{templateSuffix}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -901,7 +901,7 @@ export const V2Select = forwardRef<HTMLDivElement, V2SelectProps>((props, ref) =
|
||||||
): SelectOption[] => {
|
): SelectOption[] => {
|
||||||
const result: SelectOption[] = [];
|
const result: SelectOption[] = [];
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
const prefix = depth > 0 ? " ".repeat(depth) + "└ " : "";
|
const prefix = depth > 0 ? "\u00A0\u00A0\u00A0".repeat(depth) + "└ " : "";
|
||||||
result.push({
|
result.push({
|
||||||
value: item.valueCode, // 🔧 valueCode를 value로 사용
|
value: item.valueCode, // 🔧 valueCode를 value로 사용
|
||||||
label: prefix + item.valueLabel,
|
label: prefix + item.valueLabel,
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,16 @@ export function useDialogAutoValidation(contentEl: HTMLElement | null) {
|
||||||
return el instanceof HTMLSelectElement && el.hasAttribute("aria-hidden");
|
return el instanceof HTMLSelectElement && el.hasAttribute("aria-hidden");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 복합 입력 필드(채번 세그먼트 등)의 시각적 테두리 컨테이너 탐지
|
||||||
|
// input 자체에 border가 없고 부모가 border를 가진 경우 부모를 반환
|
||||||
|
function findBorderContainer(input: TargetEl): HTMLElement {
|
||||||
|
const parent = input.parentElement;
|
||||||
|
if (parent && parent.classList.contains("border")) {
|
||||||
|
return parent;
|
||||||
|
}
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
function isEmpty(input: TargetEl): boolean {
|
function isEmpty(input: TargetEl): boolean {
|
||||||
if (input instanceof HTMLButtonElement) {
|
if (input instanceof HTMLButtonElement) {
|
||||||
// Radix Select: data-placeholder 속성이 자식 span에 있으면 미선택 상태
|
// Radix Select: data-placeholder 속성이 자식 span에 있으면 미선택 상태
|
||||||
|
|
@ -120,20 +130,24 @@ export function useDialogAutoValidation(contentEl: HTMLElement | null) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function markError(input: TargetEl) {
|
function markError(input: TargetEl) {
|
||||||
input.setAttribute(ERROR_ATTR, "true");
|
const container = findBorderContainer(input);
|
||||||
|
container.setAttribute(ERROR_ATTR, "true");
|
||||||
errorFields.add(input);
|
errorFields.add(input);
|
||||||
showErrorMsg(input);
|
showErrorMsg(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearError(input: TargetEl) {
|
function clearError(input: TargetEl) {
|
||||||
input.removeAttribute(ERROR_ATTR);
|
const container = findBorderContainer(input);
|
||||||
|
container.removeAttribute(ERROR_ATTR);
|
||||||
errorFields.delete(input);
|
errorFields.delete(input);
|
||||||
removeErrorMsg(input);
|
removeErrorMsg(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 빈 필수 필드 아래에 경고 문구 삽입 (레이아웃 영향 없는 zero-height wrapper)
|
// 빈 필수 필드 아래에 경고 문구 삽입 (레이아웃 영향 없는 zero-height wrapper)
|
||||||
|
// 복합 입력(채번 세그먼트 등)은 border 컨테이너 바깥에 삽입
|
||||||
function showErrorMsg(input: TargetEl) {
|
function showErrorMsg(input: TargetEl) {
|
||||||
if (input.parentElement?.querySelector(`.${MSG_WRAPPER_CLASS}`)) return;
|
const container = findBorderContainer(input);
|
||||||
|
if (container.parentElement?.querySelector(`.${MSG_WRAPPER_CLASS}`)) return;
|
||||||
|
|
||||||
const wrapper = document.createElement("div");
|
const wrapper = document.createElement("div");
|
||||||
wrapper.className = MSG_WRAPPER_CLASS;
|
wrapper.className = MSG_WRAPPER_CLASS;
|
||||||
|
|
@ -142,17 +156,19 @@ export function useDialogAutoValidation(contentEl: HTMLElement | null) {
|
||||||
msg.textContent = "필수 입력 항목입니다";
|
msg.textContent = "필수 입력 항목입니다";
|
||||||
wrapper.appendChild(msg);
|
wrapper.appendChild(msg);
|
||||||
|
|
||||||
input.insertAdjacentElement("afterend", wrapper);
|
container.insertAdjacentElement("afterend", wrapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeErrorMsg(input: TargetEl) {
|
function removeErrorMsg(input: TargetEl) {
|
||||||
const wrapper = input.parentElement?.querySelector(`.${MSG_WRAPPER_CLASS}`);
|
const container = findBorderContainer(input);
|
||||||
|
const wrapper = container.parentElement?.querySelector(`.${MSG_WRAPPER_CLASS}`);
|
||||||
if (wrapper) wrapper.remove();
|
if (wrapper) wrapper.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
function highlightField(input: TargetEl) {
|
function highlightField(input: TargetEl) {
|
||||||
input.setAttribute(HIGHLIGHT_ATTR, "true");
|
const container = findBorderContainer(input);
|
||||||
input.addEventListener("animationend", () => input.removeAttribute(HIGHLIGHT_ATTR), { once: true });
|
container.setAttribute(HIGHLIGHT_ATTR, "true");
|
||||||
|
container.addEventListener("animationend", () => container.removeAttribute(HIGHLIGHT_ATTR), { once: true });
|
||||||
|
|
||||||
if (input instanceof HTMLButtonElement) {
|
if (input instanceof HTMLButtonElement) {
|
||||||
input.click();
|
input.click();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue